registry/docs/reference/cli/commands.md
Yusufhan Sacak d1dcaf3fb3
Some checks failed
CI Pipeline / Build, Lint, and Validate (push) Has been cancelled
CI Pipeline / Tests (push) Has been cancelled
Deploy to Staging / Build and Push Image with ko (push) Has been cancelled
Deploy to Staging / Deploy to Staging (push) Has been cancelled
fix(publisher): tolerate transient device-flow failures during login polling (#1588)
Follow-up to my analysis on #1543.

Heads up on the shape first: this builds on #1290's poll-loop seams and
github wont let me target a fork branch, so the diff shows its two
commits too. Review only the last commit, the first two are #1290
unchanged, credit to @developer-ishan. Happy to rebase whichever way
#1290 goes, or split differently if that's easier to review.

What it does. pollForToken treats every non-pending answer as terminal,
so one flaky response kills a login the user may already have approved
in the browser. This spends a small grace budget (2 polls) on
incorrect_device_code before giving up, retries 5xx with a growing
backoff, and returns status plus body on other non-200s instead of dying
inside json.Unmarshal on an html error page. Terminal errors now carry
error_description and error_uri, and a timeout that happens after
polling recovered reports a plain timeout, not a stale error from
minutes earlier.

Also honours the interval and expires_in GitHub returns with the device
code (RFC 8628 3.2) instead of hardcoding 5/900, clamped (60s / 3600s)
so a misbehaving response can't stall the CLI or overflow the duration
math.

One thing I changed my mind on from the issue comment: I wrote there
that the client should wait one interval before the first poll. Dropped
that. My own repro test showed 40s of clean polling before authorize, so
the propagation theory doesn't hold, and the grace retry covers the same
window without taxing every fast login. First poll stays immediate,
pinned by a test.

Root cause note: I can't prove the GitHub-side race (cli/cli#9302 is the
same error class but was closed on an unconfirmed "platform race"). So
this isn't claimed as a root-cause fix, it makes the login recoverable
if the condition is transient and diagnosable if it isn't. Btw
incorrect_device_code is a GitHub extension, not an RFC 8628 3.5 code,
so there's no spec retry semantic being violated by the grace polls.

## Test

12 new tests in github_at_device_flow_internal_test.go, all green with
-race (33 in the package). #1290's four tests pass unmodified.
golangci-lint clean in the touched files under both v2.11.4 (this
branch's CI pin) and v2.13.1 (what main pins now).

## AI Disclosure

AI assistance (Claude) was used for issue research and during
development. All changes were reviewed and tested by the author.

Fixes #1543

---------

Co-authored-by: developer-ishan <igupta@groupon.com>
Co-authored-by: Ishan Gupta <62565307+developer-ishan@users.noreply.github.com>
Co-authored-by: developer-ishan <guptaishan849@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-16 18:42:30 +03:00

12 KiB

Publisher CLI Commands Reference

Complete command reference for the mcp-publisher CLI tool.

See the publishing guide for a walkthrough of using the CLI to publish a server.

Installation

Install via Homebrew (macOS/Linux):

$ brew install mcp-publisher

Global Options

All commands support:

  • --help, -h - Show command help

--registry is a flag on login only (default: https://registry.modelcontextprotocol.io). The other commands read the registry URL from the stored login token, so passing --registry to publish would be interpreted as the server.json path.

Commands

mcp-publisher init

Generate a server.json template with automatic detection.

Usage:

mcp-publisher init

Behavior:

  • Creates server.json in current directory
  • Auto-detects package managers (package.json, setup.py, etc.)
  • Pre-fills fields where possible
  • Writes TODO: placeholders for fields it cannot detect — it is non-interactive and takes no flags

Example output:

{
  "name": "io.github.username/server-name",
  "description": "TODO: Add server description",
  "version": "1.0.0",
  "packages": [
    {
      "registryType": "npm",
      "identifier": "detected-package-name",
      "version": "1.0.0"
    }
  ]
}

mcp-publisher login <method>

Authenticate with the registry.

Authentication Methods:

GitHub Interactive

mcp-publisher login github [--token=PAT] [--registry=URL]
  • Opens browser for GitHub OAuth flow
  • Grants access to io.github.{username}/* and io.github.{org}/* namespaces
  • --token supplies a GitHub Personal Access Token instead of the interactive flow, which is how publishing from GitHub Actions authenticates without a browser. This flag is accepted by login github only.

Interactive login handles retryable OAuth errors in both HTTP 200 and HTTP 400 responses. Access denial, expired codes, and unrecognized OAuth errors still stop polling; an incorrect_device_code error receives at most two additional attempts per login.

GitHub OIDC (CI/CD)

mcp-publisher login github-oidc [--registry=URL]
  • Uses GitHub Actions OIDC tokens automatically
  • Requires id-token: write permission in workflow
  • No browser interaction needed

The CLI derives the OIDC aud claim from --registry (scheme + host, e.g. https://registry.modelcontextprotocol.io) so tokens are bound to the specific deployment they were minted for. Self-hosters must set MCP_REGISTRY_GITHUB_OIDC_AUDIENCE on the registry to the matching value; publishers running an older mcp-publisher will fail with invalid audience and need to upgrade.

Also see the guide to publishing from GitHub Actions.

DNS Verification

mcp-publisher login dns --domain=example.com --private-key=HEX_KEY [--algorithm=ed25519|ecdsap384] [--registry=URL]
  • Verifies domain ownership via DNS TXT record
  • Grants access to com.example.* namespaces
  • Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex)
    • --algorithm defaults to ed25519. For an ECDSA P-384 key you must pass --algorithm ecdsap384, otherwise the key is rejected with invalid seed length: expected 32 bytes, got 48.
    • The private key can be stored in a cloud signing provider like Google KMS or Azure Key Vault. Cloud providers derive the algorithm from the key itself, so --algorithm does not apply to them.

Setup: (for Ed25519, recommended)

# Generate keypair
openssl genpkey -algorithm Ed25519 -out key.pem

# Get public key for DNS record
openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64

# Add DNS TXT record:
# example.com. IN TXT "v=MCPv1; k=ed25519; p=PUBLIC_KEY"

# Extract private key for login
openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n'

Setup: (for ECDSA P-384)

# Generate keypair
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out key.pem

# Get public key for DNS record
openssl ec -in key.pem -text -noout -conv_form compressed | grep -A4 "pub:" | tail -n +2 | tr -d ' :\n' | xxd -r -p | base64

# Add DNS TXT record:
# example.com. IN TXT "v=MCPv1; k=ecdsap384; p=PUBLIC_KEY"

# Extract private key for login
openssl ec -in <pem path> -noout -text | grep -A4 "priv:" | tail -n +2 | tr -d ' :\n'

# Log in, selecting the ECDSA P-384 algorithm explicitly
mcp-publisher login dns --algorithm ecdsap384 --domain=example.com --private-key=HEX_KEY

Setup: (for Google KMS signing)

This requires the gcloud CLI.

# log in and set default project
gcloud auth login
gcloud config set project myproject

# Create a keyring in your project
gcloud kms keyrings create mykeyring --location global

# Create an Ed25519 signing key
gcloud kms keys create mykey --default-algorithm=ec-sign-ed25519 --purpose=asymmetric-signing --keyring=mykeyring --location=global

# Enable Application Default Credentials (ADC) so the publisher tool can sign
gcloud auth application-default login

# Attempt login to show the public key
mcp-publisher login dns google-kms --domain=example.com --resource=projects/myproject/locations/global/keyRings/mykeyring/cryptoKeys/mykey/cryptoKeyVersions/1

# Copy the "Expected proof record" and add the TXT record
# example.com. IN TXT "v=MCPv1; k=ed25519; p=PUBLIC_KEY"

# Re-run the login command
mcp-publisher login dns google-kms --domain=example.com --resource=projects/myproject/locations/global/keyRings/mykeyring/cryptoKeys/mykey/cryptoKeyVersions/1

Setup: (for Azure Key Vault signing)

This requires the Azure CLI.

# log in and set default subscription
az login
az account set --subscription "My Subscription (name or ID)"

# Create a resource group
az group create --location westus --resource-group MyResourceGroup

# Create a Key Vault
az keyvault create --name MyKeyVault --location westus --resource-group MyResourceGroup

# Create an ECDSA P-384 signing key
az keyvault key create --name MyKey --vault-name MyKeyVault --curve P-384

# Attempt login to show the public key
mcp-publisher login dns azure-key-vault --domain=example.com --vault MyKeyVault --key MyKey

# Copy the "Expected proof record" and add the TXT record
# example.com. IN TXT "v=MCPv1; k=ecdsap384; p=PUBLIC_KEY"

# Re-run the login command
mcp-publisher login dns azure-key-vault --domain=example.com --vault MyKeyVault --key MyKey

HTTP Verification

mcp-publisher login http --domain=example.com --private-key=HEX_KEY [--algorithm=ed25519|ecdsap384] [--registry=URL]
  • Verifies domain ownership via HTTPS endpoint
  • Grants access to com.example.* namespaces
  • Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex)
    • --algorithm defaults to ed25519. For an ECDSA P-384 key you must pass --algorithm ecdsap384.
    • The private key can be stored in a cloud signing provider like Google KMS or Azure Key Vault. Cloud providers derive the algorithm from the key itself, so --algorithm does not apply to them.

Setup: (for Ed25519, recommended)

# Generate keypair (same as DNS)
openssl genpkey -algorithm Ed25519 -out key.pem

# Host public key at:
# https://example.com/.well-known/mcp-registry-auth
# Content: v=MCPv1; k=ed25519; p=PUBLIC_KEY

Setup: (for ECDSA P-384)

# Generate keypair (same as DNS)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out key.pem

# Host public key at:
# https://example.com/.well-known/mcp-registry-auth
# Content: v=MCPv1; k=ecdsap384; p=PUBLIC_KEY

# Log in, selecting the ECDSA P-384 algorithm explicitly
mcp-publisher login http --algorithm ecdsap384 --domain=example.com --private-key=HEX_KEY

Cloud signing is also supported for HTTP authentication, similar to the DNS examples above. Just swap out the dns positional argument for http.

Anonymous (Testing)

mcp-publisher login none [--registry=URL]
  • No authentication - for local testing only
  • Only works with local registry instances

mcp-publisher validate

Validate a server.json file without publishing.

Usage:

mcp-publisher validate [file]

Arguments:

  • file - Path to server.json file (default: ./server.json)

Behavior:

  • Performs exhaustive validation, reporting all issues at once (not just the first error)
  • Validates JSON syntax and schema compliance
  • Runs semantic validation (business logic checks)
  • Checks for deprecated schema versions and provides migration guidance
  • Includes detailed error locations with JSON paths (e.g., packages[0].transport.url)
  • Shows validation issue type (json, schema, semantic, linter)
  • Displays severity level (error, warning, info)
  • Provides schema references showing which validation rule triggered each error

Example output:

$ mcp-publisher validate
✅ server.json is valid

$ mcp-publisher validate custom-server.json
❌ Validation failed with 2 issue(s):

1. [error] repository.url (schema)
   '' has invalid format 'uri'
   Reference: #/definitions/Repository/properties/url/format from: [#/definitions/ServerDetail]/properties/repository/[#/definitions/Repository]/properties/url/format

2. [error] name (semantic)
   server name must be in format 'dns-namespace/name'
   Reference: invalid-server-name

mcp-publisher publish

Publish server to the registry.

For detailed guidance on the publishing process, see the publishing guide.

Usage:

mcp-publisher publish [PATH]

Options:

  • PATH - Path to server.json (default: ./server.json)

Process:

  1. Validates server.json against schema
  2. Publishes the server.json to the registry server URL specified in the login token
  3. Server: Verifies package ownership (see Official Registry Requirements)
  4. Server: Checks namespace authentication
  5. Server: Publishes to registry

Example:

# Basic publish
mcp-publisher publish

# Custom file location  
mcp-publisher publish ./config/server.json

mcp-publisher status

Update the lifecycle status of a published server.

Usage:

mcp-publisher status --status <active|deprecated|deleted> [flags] <server-name> [version]

Flags:

  • --status (required) - New status: active, deprecated, or deleted
  • --message - Optional message explaining the status change (not allowed when status is active)
  • --all-versions - Apply status change to all versions of the server
  • --yes, -y - Skip confirmation prompt (only applies when using --all-versions)

Arguments:

  • server-name - Full server name (e.g., io.github.user/my-server)
  • version - Server version to update (required unless --all-versions is set)

Status Values:

  • active - Server is active and visible in default listings
  • deprecated - Server is deprecated but still visible with a warning message
  • deleted - Server is hidden from default listings

Examples:

# Deprecate a specific version
mcp-publisher status --status deprecated --message "Please upgrade to 2.0.0" \
  io.github.user/my-server 1.0.0

# Delete a version with security issues
mcp-publisher status --status deleted --message "Critical security vulnerability" \
  io.github.user/my-server 1.0.0

# Restore a version to active
mcp-publisher status --status active io.github.user/my-server 1.0.0

# Deprecate all versions at once
mcp-publisher status --status deprecated --all-versions --message "Project archived" \
  io.github.user/my-server

Requirements:

  • Must be logged in with publish or edit permission for the server namespace

mcp-publisher logout

Clear stored authentication credentials.

Usage:

mcp-publisher logout

Behavior:

  • Removes ~/.config/mcp-publisher/token.json
  • Also cleans up legacy token files (~/.mcp_publisher_token, .mcpregistry_*)
  • Does not revoke tokens on server side

Configuration

Token Storage

Authentication tokens are stored in ~/.config/mcp-publisher/token.json as JSON:

{
  "token": "jwt-token-here",
  "method": "github",
  "registry": "https://registry.modelcontextprotocol.io"
}

Note: Tokens were previously stored in ~/.mcp_publisher_token. If you are upgrading, run mcp-publisher logout followed by mcp-publisher login to migrate to the new location.