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

375 lines
12 KiB
Markdown

# Publisher CLI Commands Reference
Complete command reference for the `mcp-publisher` CLI tool.
See the [publishing guide](../../modelcontextprotocol-io/quickstart.mdx) for a walkthrough of using the CLI to publish a server.
## Installation
Install via Homebrew (macOS/Linux):
```bash
$ 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:**
```bash
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:**
```json
{
"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
```bash
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](../../modelcontextprotocol-io/github-actions.mdx) 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)
```bash
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](../../modelcontextprotocol-io/github-actions.mdx).
#### DNS Verification
```bash
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)
```bash
# 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)
```bash
# 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](https://cloud.google.com/sdk/docs/install).
```bash
# 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](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest).
```bash
# 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
```bash
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)
```bash
# 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)
```bash
# 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)
```bash
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:**
```bash
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:**
```bash
$ 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](../../modelcontextprotocol-io/quickstart.mdx).
**Usage:**
```bash
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](../server-json/official-registry-requirements.md))
4. Server: Checks namespace authentication
5. Server: Publishes to registry
**Example:**
```bash
# 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:**
```bash
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:**
```bash
# 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:**
```bash
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:
```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.