name: Publish to MCP Registry

# Auto-publishes .mcp/server.json to registry.modelcontextprotocol.io
# AFTER the Release workflow finishes successfully. This sequencing
# matters: the MCP Registry validators check that the npm package
# referenced in server.json actually exists at the declared version
# — and that's only true once Release's goreleaser step has finished
# the npm publish. Pre-this-change we triggered on the same tag push
# that triggered Release, which raced (v0.11.0 and v0.12.0 both hit
# this).
#
# Authenticates via GitHub OIDC against the io.github.tosin2013/*
# namespace (no PAT or stored secret needed — the id-token: write
# permission is sufficient).
#
# Manual run is also supported via workflow_dispatch — useful when you
# need to re-publish without cutting a tag (e.g. metadata-only fix).
#
# What this workflow does NOT do:
#   - Submit to Smithery, mcp.so, Glama, PulseMCP. Those are separate
#     registries with separate submission flows. mcp.so / Glama /
#     PulseMCP auto-ingest from the official registry on a schedule, so
#     publishing here is enough for those. Smithery requires a
#     smithery.yaml + a hosted-runtime story we don't have today; see
#     docs/howto/register-with-mcp-clients.md for the manual path.
#   - Submit a PR to modelcontextprotocol/servers (the awesome-list).
#     That repo prefers a single PR adding the row by hand; not worth
#     automating for one entry.

on:
  # Chain off the Release workflow: when it finishes, we run. This
  # closes the npm-publish race that hit v0.11.0 and v0.12.0.
  # workflow_run fires on every completion; the job-level `if:` below
  # filters to tag-push-triggered Release runs that succeeded.
  workflow_run:
    workflows: ["Release"]
    types: [completed]
  workflow_dispatch:
    inputs:
      version_override:
        description: 'Version to publish (e.g. 0.12.0). If empty, uses the version field already in .mcp/server.json'
        required: false
        type: string

jobs:
  publish:
    name: Publish server.json
    runs-on: ubuntu-latest
    # When chained off Release, only run when Release succeeded AND
    # was triggered by a tag push (skip dry-run workflow_dispatch
    # invocations of Release). workflow_dispatch invocations of THIS
    # workflow bypass the filter — operators can always re-fire
    # manually.
    if: >-
      github.event_name == 'workflow_dispatch' ||
      (github.event_name == 'workflow_run' &&
       github.event.workflow_run.conclusion == 'success' &&
       startsWith(github.event.workflow_run.head_branch, 'v'))
    permissions:
      id-token: write   # Required for GitHub OIDC authentication
      contents: read    # Required for actions/checkout

    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          # When chained off Release, default ref is the default
          # branch — not the tag. Check out the tag the Release run
          # was for so the .mcp/server.json we read matches the
          # release content.
          ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref }}

      - name: Install jq
        run: sudo apt-get update && sudo apt-get install -y jq

      - name: Install mcp-publisher
        run: |
          set -euo pipefail
          OS=$(uname -s | tr '[:upper:]' '[:lower:]')
          ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
          curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${OS}_${ARCH}.tar.gz" \
            | tar xz mcp-publisher
          chmod +x mcp-publisher
          ./mcp-publisher --help | head -5

      - name: Sync version with the tag
        env:
          # workflow_run event carries the tag name in head_branch
          # (yes, "head_branch" — workflow_run uses branch
          # terminology for tag-push triggers, that's the GitHub
          # event-payload quirk). Strip leading 'v' below.
          UPSTREAM_REF: ${{ github.event.workflow_run.head_branch }}
        run: |
          set -euo pipefail
          # Three trigger paths, three version derivations:
          #   workflow_run         — upstream tag from head_branch (strip v)
          #   workflow_dispatch    — inputs.version_override (may be empty)
          if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
            VERSION="${UPSTREAM_REF#v}"
          else
            VERSION="${{ inputs.version_override }}"
          fi

          if [[ -n "$VERSION" ]]; then
            echo "Setting .mcp/server.json version to $VERSION"
            # Two conventions per ADR 030 + the registry validators:
            #   - npm packages: identifier=name, version=$v as a separate field
            #   - oci packages: version is embedded in identifier as :tag, no
            #     separate version field allowed
            jq --arg v "$VERSION" '
              .version = $v
              | .packages |= map(
                  if .registryType == "oci" then
                    .identifier = (.identifier | sub(":[^:/]+$"; "")) + ":" + $v
                    | del(.version)
                  else
                    .version = $v
                  end
                )' \
               .mcp/server.json > .mcp/server.tmp.json
            mv .mcp/server.tmp.json .mcp/server.json
          fi

          # Show the final document for the workflow log — easy to audit
          # what got submitted by reading the run output.
          echo "--- final .mcp/server.json ---"
          cat .mcp/server.json

      - name: Validate server.json against the upstream schema
        run: |
          set -euo pipefail
          npm init -y >/dev/null
          npm install --silent ajv ajv-formats
          # Fetch the dated schema referenced by .mcp/server.json's $schema
          # field so our local validation matches what mcp-publisher does
          # server-side (it rejects URLs it can't extract a version from).
          SCHEMA_URL=$(jq -r '."$schema"' .mcp/server.json)
          echo "Validating against: $SCHEMA_URL"
          curl -fsSL "$SCHEMA_URL" -o schema.json
          node -e "
            const Ajv = require('ajv');
            const addFormats = require('ajv-formats');
            const fs = require('fs');
            const schema = JSON.parse(fs.readFileSync('schema.json'));
            const data = JSON.parse(fs.readFileSync('.mcp/server.json'));
            const ajv = new Ajv({ strict: false, allErrors: true });
            addFormats(ajv);
            const validate = ajv.compile(schema);
            if (!validate(data)) {
              console.error('Schema validation FAILED:');
              console.error(JSON.stringify(validate.errors, null, 2));
              process.exit(1);
            }
            console.log('Schema VALID ✓');
          "

      - name: Wait for npm package to be reachable
        # Belt-and-suspenders: workflow_run sequencing already
        # guarantees Release's npm publish completed before we run,
        # but the npm registry CDN takes a few seconds to propagate.
        # Poll until the version is visible (or 2 min cap) so we
        # don't surface a misleading 404 to mcp-publisher.
        run: |
          set -euo pipefail
          NAME=$(jq -r '.packages[] | select(.registryType == "npm") | .identifier' .mcp/server.json | head -1)
          VERSION=$(jq -r '.packages[] | select(.registryType == "npm") | .version' .mcp/server.json | head -1)
          if [[ -z "$NAME" || -z "$VERSION" ]]; then
            echo "No npm package in server.json — skipping precheck."
            exit 0
          fi
          echo "Waiting for $NAME@$VERSION on npm..."
          for i in $(seq 1 24); do  # 24 × 5s = 2 min cap
            if curl -fsS "https://registry.npmjs.org/$NAME/$VERSION" >/dev/null 2>&1; then
              echo "Found $NAME@$VERSION ✓"
              exit 0
            fi
            echo "  attempt $i: not yet visible, sleeping 5s"
            sleep 5
          done
          echo "::error::$NAME@$VERSION still not visible on npm after 2 min."
          echo "::error::Either Release's npm publish failed, or this workflow was run before Release completed."
          echo "::error::Re-run after verifying npm view $NAME versions includes $VERSION."
          exit 1

      - name: Authenticate via GitHub OIDC
        run: ./mcp-publisher login github-oidc

      - name: Publish to MCP Registry
        run: ./mcp-publisher publish .mcp/server.json

      - name: Workflow summary
        if: always()
        run: |
          NAME=$(jq -r .name .mcp/server.json)
          VERSION=$(jq -r .version .mcp/server.json)
          {
            echo "### MCP Registry submission"
            echo ""
            echo "- **Server:** \`$NAME\`"
            echo "- **Version:** \`$VERSION\`"
            echo "- **Status:** ${{ job.status }}"
            echo ""
            if [[ "${{ job.status }}" == "success" ]]; then
              SEARCH_URL="https://registry.modelcontextprotocol.io/v0/servers?search=$(echo $NAME | sed 's|/|%2F|g')"
              echo "Verify the listing via the search API:"
              echo "  $SEARCH_URL"
              echo ""
              echo "(The official registry has no human-facing web UI today — it's"
              echo "an API-only metadata service. Browse via downstream aggregators"
              echo "instead: mcp.so, Glama, PulseMCP. They typically ingest within 24h.)"
            fi
          } >> "$GITHUB_STEP_SUMMARY"
