Close invalid publish PRs #100
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Close invalid publish PRs | |
| # Stage 2 of 2 (see detect-invalid-publish-prs.yml for stage 1). | |
| # | |
| # Runs in the trusted base-repo context (triggered by workflow_run, not by the | |
| # PR itself), so it has a write token. It picks up the PR number flagged by | |
| # stage 1 and, after independently re-validating the PR from the API, posts a | |
| # comment pointing at the publishing docs, labels it `invalid`, and closes it. | |
| # | |
| # Trust boundary: the ONLY thing crossing from the untrusted stage is the PR | |
| # number (sanitized to digits below). We re-derive every decision (state, | |
| # author, changed files) here, so a wrong/stale artifact can never cause us to | |
| # close a PR that doesn't actually match. | |
| on: | |
| workflow_run: | |
| workflows: ["Detect invalid publish PRs"] | |
| types: [completed] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write # labels on PRs go through the issues API | |
| actions: read # needed to list/download the stage-1 artifact | |
| jobs: | |
| close: | |
| if: github.event.workflow_run.conclusion == 'success' | |
| runs-on: ubuntu-latest | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| COMMENT_BODY: | | |
| Hi @__AUTHOR__ 👋 — thanks for your interest in the MCP Registry! | |
| It looks like this PR is trying to publish an MCP server by adding or editing server files in this repository (under `servers/`, `data/servers/`, or in `data/seed.json`). That isn't how servers get published, so I'm closing this PR automatically. | |
| **Servers are published with the [`mcp-publisher`](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx) CLI**, not by opening a pull request against this repo. The CLI verifies that you own your namespace and submits your `server.json` directly to the live registry API. | |
| To publish your server, follow the **[Publishing Quickstart](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx)**. In short: | |
| ```bash | |
| # Build the publisher CLI | |
| make publisher | |
| # Authenticate (e.g. via GitHub) and publish your server.json | |
| ./bin/mcp-publisher login github | |
| ./bin/mcp-publisher publish | |
| ``` | |
| Note: `data/seed.json` is seed data for **local development only** — adding entries there does not publish anything to the registry. | |
| If you believe this was closed in error, please leave a comment and a maintainer will take a look. 🙇 | |
| _This is an automated message._ | |
| steps: | |
| - name: Check for flagged-pr artifact | |
| id: check | |
| run: | | |
| set -euo pipefail | |
| names="$(gh api "repos/$GH_REPO/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[].name' || true)" | |
| if printf '%s\n' "$names" | grep -qx "flagged-pr"; then | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "No flagged-pr artifact on run $RUN_ID; nothing to do." | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Download flagged PR number | |
| if: steps.check.outputs.found == 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: flagged-pr | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ github.token }} | |
| - name: Re-validate and close | |
| if: steps.check.outputs.found == 'true' | |
| run: | | |
| set -euo pipefail | |
| # Sanitize: the only cross-boundary input. Keep digits only. | |
| PR_NUMBER="$(tr -dc '0-9' < pr-number.txt)" | |
| if [ -z "$PR_NUMBER" ]; then | |
| echo "No valid PR number in artifact; skipping." | |
| exit 0 | |
| fi | |
| echo "Re-validating PR #$PR_NUMBER" | |
| state="$(gh pr view "$PR_NUMBER" --json state --jq '.state')" | |
| if [ "$state" != "OPEN" ]; then | |
| echo "PR #$PR_NUMBER is $state, not OPEN; skipping." | |
| exit 0 | |
| fi | |
| # Use the REST endpoint: `gh pr view --json` does not expose an | |
| # authorAssociation field, so calling it there exits non-zero and, | |
| # under `set -euo pipefail`, aborts the job before we ever close. | |
| assoc="$(gh api "repos/$GH_REPO/pulls/$PR_NUMBER" --jq '.author_association')" | |
| case "$assoc" in | |
| MEMBER|OWNER|COLLABORATOR) | |
| echo "Author association '$assoc' is trusted; skipping." | |
| exit 0 | |
| ;; | |
| esac | |
| # Idempotency: if a maintainer reopens after we labeled it, leave it be. | |
| # Capture into a var first so a transient gh failure can't fail open | |
| # (an errored fetch piped to grep would otherwise look "not labeled"). | |
| labels="$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name' || true)" | |
| if printf '%s\n' "$labels" | grep -qx "invalid"; then | |
| echo "PR #$PR_NUMBER already labeled 'invalid'; skipping." | |
| exit 0 | |
| fi | |
| # Paginated REST endpoint (gh pr view --json files caps at 100, no pagination). | |
| mapfile -t FILES < <(gh api --paginate "repos/$GH_REPO/pulls/$PR_NUMBER/files" --jq '.[].filename') | |
| if [ "${#FILES[@]}" -eq 0 ]; then | |
| echo "No files for PR #$PR_NUMBER; skipping." | |
| exit 0 | |
| fi | |
| # Keep this list in sync with the copy in stage 1. | |
| publish_attempt=0 | |
| other=0 | |
| for f in "${FILES[@]}"; do | |
| if [[ "$f" == servers/* \ | |
| || "$f" == data/servers/* \ | |
| || "$f" == "data/seed.json" \ | |
| || "$f" == "server.json" \ | |
| || "$f" == "servers.json" ]]; then | |
| publish_attempt=1 | |
| else | |
| other=1 | |
| fi | |
| done | |
| if [ "$publish_attempt" -ne 1 ] || [ "$other" -ne 0 ]; then | |
| echo "PR #$PR_NUMBER no longer matches the publish-attempt pattern; skipping." | |
| exit 0 | |
| fi | |
| author="$(gh pr view "$PR_NUMBER" --json author --jq '.author.login')" | |
| body="${COMMENT_BODY//__AUTHOR__/$author}" | |
| echo "Commenting on, labeling, and closing PR #$PR_NUMBER" | |
| gh pr comment "$PR_NUMBER" --body "$body" | |
| gh pr edit "$PR_NUMBER" --add-label "invalid" || echo "Could not add label; continuing." | |
| gh pr close "$PR_NUMBER" |