Some checks failed
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>
375 lines
12 KiB
Markdown
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.
|