Skip to content

[Discovery] Publish to the MCP Registry after each stable release - #1132

Merged
Aymericr merged 5 commits into
mainfrom
cursor/mcp-registry-publish-after-release-53c2
Oct 27, 2026
Merged

Aymericr merged 5 commits into
mainfrom
cursor/mcp-registry-publish-after-release-53c2

Conversation

@Aymericr

@Aymericr Aymericr commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

Root cause: no workflow in this repo has ever published server.json to the MCP Registry. mcp-registry.yml only validates the manifest, and publishing was a manual owner step (wiki/npm-release.md). The green run after the 1.0.6 release (run 39880562788) was just the push trigger's validate job, so a green result said nothing about the registry.

Changes:

  • mcp-registry.yml also runs as a reusable workflow (workflow_call) and by manual dispatch, with version and publish inputs. version is required for dispatch, so every publish run is tied to a release tag.
    • The server name app.pascal/editor, package @pascal-app/mcp and remote https://editor.pascal.app/api/mcp are hard-coded. The version check and the full manifest check are defined once at workflow level, and both jobs run them.
    • validate checks the version input before any checkout. The version must be a stable X.Y.Z (anchored regex, no CR or LF). It then checks out the @pascal-app/mcp@<version> tag, resolves the tag to its commit, and outputs that commit SHA. It also runs the full manifest check.
    • publish (needs validate, environment mcp-registry, concurrency group mcp-registry-publish without cancel-in-progress):
      1. checks the version input, and that validate reported a 40-hex commit, before any checkout;

      2. checks out that exact commit, not the tag name. It fails if HEAD differs or the tag no longer points at that commit;

      3. repeats the full check before login, and fails on any mismatch:

        • server.json: name, version, exactly one package entry (registry type, registry base URL, identifier, version, runtime hint, transport) and exactly the one remote URL;
        • packages/mcp/package.json: name, mcpName and version.

        Later steps get the version only from this step's output, and nothing goes to $GITHUB_ENV;

      4. waits up to 10 minutes for npm view --registry https://registry.npmjs.org @pascal-app/mcp@<version> mcpName to return app.pascal/editor, run from outside the checkout;

      5. skips the publish if the registry already lists the version. Otherwise it runs mcp-publisher login dns --domain pascal.app and mcp-publisher publish;

      6. deletes ~/.config/mcp-publisher/token.json (and ~/.mcpregistry_registry_token as a fallback) in an if: always() step;

      7. fails unless /v0/servers/app.pascal%2Feditor/versions then lists the version.

    • The concurrency group keeps one running and one waiting run, so a third queued run replaces the waiting one. That is accepted: a replaced run can be dispatched again, and an already listed version is skipped.
    • hosted-matches-manifest runs only when the check-hosted dispatch input is set, and it doesn't gate the publish.
    • Unchanged: the triggers, contents: read permissions, checkout SHA pin and mcp-publisher sha256 check.
  • release.yml: a new mcp-registry job runs after release for non-dry-run latest releases. It runs only when the released @pascal-app/mcp version, taken from packages/mcp/package.json as a new mcp-version output, contains no -, so a bump=none rerun after a beta doesn't publish. It passes only MCP_REGISTRY_PRIVATE_KEY, through the declared workflow_call.secrets, instead of secrets: inherit. GitHub needs that pass-through even though the secret lives only in the environment.
  • Docs (wiki/npm-release.md, README): the automated flow, the CI-only signing key and the mcp-registry environment, key rotation, publish queueing, and recovering a missed version.

Version sourcing: this was already in place. scripts/sync-agent-versions.ts rewrites server.json from packages/mcp/package.json, and skills:validate fails on drift. This PR doesn't change it.

Server card: it is served by the hosted deployment, not this repo. Top-level "version": "1.0" is the card's format version. serverInfo.version and the API catalog follow whatever @pascal-app/mcp the deployment runs, so fixing them is a hosted-deployment change. hosted-matches-manifest flags the mismatch when dispatched.

Maintainer action needed (repository admin)

The mcp-registry environment exists, limited to main, with no required reviewer.

  1. Generate a fresh, CI-only Ed25519 keypair and add its own pascal.app TXT record (v=MCPv1; k=ed25519; p=<public key>). Neither of the two existing v=MCPv1 records on pascal.app is CI's. Record CI's public key in wiki/npm-release.md#the-ci-signing-key.
  2. Store the hex private key as MCP_REGISTRY_PRIVATE_KEY in the mcp-registry environment only. It must never be a repository secret or a secret of another environment.
  3. A tag ruleset that limits who can create @pascal-app/mcp@* tags is recommended. Setting it up needs a repository admin.

This PR adds no secrets and changes no environments or rulesets. Until the secret exists, the publish job fails with an error naming it. 1.0.6 is already in the registry, so no manual publish is needed after merge.

How to test

  1. With shellcheck 0.11.0 installed, actionlint .github/workflows/mcp-registry.yml (actionlint 1.7.7) reports nothing, and shellcheck is clean on the two shared check scripts. release.yml passes plain actionlint; its only shellcheck findings are in lines this PR doesn't touch.
  2. Local harness: it takes the version check, the full manifest check and both commit steps straight from the workflow YAML and runs them against fixtures.
    • Version input: 1.0.6 passes. These fail before checkout:
      • 1.0.6-beta.1, v1.0.6, 01.0.6 or an empty value
      • a value containing LF or CR
      • a validated commit that isn't a SHA
    • Manifest: a valid manifest passes and outputs only version=1.0.6, and so do the real @pascal-app/mcp@1.0.6 files. Each of the following fails and writes no output:
      • server.json name, version, or a multi-line version
      • package registry type, base URL, identifier, version, transport or runtime hint
      • an extra package entry
      • the remote URL, or an extra remote
      • package.json name, mcpName or version
    • Commit hand-off: run against a local git origin. validate resolves the tag and outputs its commit. publish passes when it is handed that commit, and refuses when it is handed a different commit, when the checkout differs from the commit it was handed, or when the tag is moved after validation. The commit check also passes against the live @pascal-app/mcp@1.0.6 tag.
  3. On this PR, MCP Registry runs validate only. publish and hosted-matches-manifest are skipped.

I did not run a registry publish.

Screenshots / screen recording

N/A — non-visual change

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch
Open in Web Open in Cursor 

Note

Medium Risk
Introduces automated registry publishing with a DNS signing secret and strict tag/commit pinning; misconfiguration or key issues could block releases or publish wrong manifests until admins complete environment setup.

Reviewed by Cursor Bugbot for commit d2e96ca. Bugbot is set up for automated code reviews on this repo. Configure here.

The MCP Registry workflow only ever validated server.json; nothing published it.
Release now calls it with the released version: it checks out the release tag,
waits for npm to serve @pascal-app/mcp with its mcpName, publishes, and fails
unless the registry lists the version.
@Aymericr Aymericr added the bot:discovery label Oct 23, 2026 — with Cursor
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T13:08:30.428735Z e38db49 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Aymericr Aymericr left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed, and this looks right to me.

  • release already exposes version and channel, and every package is tagged in lockstep, so refs/tags/@pascal-app/mcp@<version> will exist when the mcp-registry job runs (@pascal-app/mcp@1.0.6 is there today).
  • The registry check before publishing makes reruns safe, and the final listing check means a green run actually proves the version is in the registry.
  • hosted-matches-manifest keying off inputs.check-hosted keeps it out of push, PR, and workflow_call runs, which is what we want.

One small nit, not blocking: a manual dispatch with publish=true and no version publishes whatever server.json is on the dispatched ref without the tag check. Making version required whenever publish is set would close that gap.

Before this is useful, MCP_REGISTRY_PRIVATE_KEY has to be added (repo or npm environment). Until then the next stable release will finish with a red mcp-registry job after npm has already published. After merge, 1.0.6 still needs a one-time dispatch. I'll merge once quality, cli-smoke, and Bugbot are green.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e38db492f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml Outdated
# A beta leaves server.json on the last stable version, which is already listed.
mcp-registry:
needs: release
if: inputs.dry-run == false && needs.release.outputs.channel == 'latest'

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 Badge Exclude prerelease recovery runs from registry publishing

When a partial beta release is recovered with the documented bump=none, the release keeps its *-beta.* version but NPM_TAG/the channel output is set to latest because only the literal beta input selects the beta channel. This condition therefore invokes the registry workflow with the prerelease version, while sync-agent-versions.ts intentionally leaves server.json on the previous stable version; the called workflow then fails its VERSION != EXPECTED_VERSION check. A recovery that otherwise successfully publishes or tags the beta is consequently reported failed. Gate this job on the released version containing no prerelease suffix, or expose an explicit stable-release output instead of inferring stability from channel.

Useful? React with 👍 / 👎.

cursoragent and others added 2 commits October 27, 2026 13:50
…ublish-after-release-53c2

Co-authored-by: Aymeric Rabot <Aymericr@users.noreply.github.com>
…pe the signing key

- Hard-code the server name and package, check the version is a stable
  semver with no line breaks, and pass it to later steps as a step output.
- Run publish in the mcp-registry environment with a concurrency group, and
  delete the registry token afterwards.
- Require version on manual dispatch; pass only MCP_REGISTRY_PRIVATE_KEY
  from release.yml instead of inheriting every secret.
- Document the CI-only DNS key, its environment and its rotation.

Co-authored-by: Aymeric Rabot <Aymericr@users.noreply.github.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 04c9e4c. Configure here.

Comment thread .github/workflows/release.yml Resolved
- validate resolves the release tag to its commit and outputs it; publish
  checks out that commit, confirms the tag still points at it, and repeats
  the full manifest check (name, version, package entry, remote) before login.
- Check the version input before any checkout, in both jobs.
- Delete ~/.config/mcp-publisher/token.json after publishing, keeping the
  older token path as a fallback.
- Query npm at registry.npmjs.org from outside the checkout.
- release.yml: call the registry workflow only for a stable @pascal-app/mcp
  version.
- Docs: the mcp-registry environment as it is set up, the secret's location,
  and how publish runs queue.

Co-authored-by: Aymeric Rabot <Aymericr@users.noreply.github.com>
@Aymericr
Aymericr merged commit 2d783fc into main Oct 27, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants