fix(publisher): document --algorithm for ECDSA login and fix validate --help (#1519)

Two real defects, plus the CLI reference claims that go with them.

## 1. ECDSA P-384 login instructions cannot work

Every "ECDSA P-384" tab ran the *identical* command to the Ed25519 tab.
Since `--algorithm` defaults to ed25519
([login.go:100](https://github.com/modelcontextprotocol/registry/blob/main/cmd/publisher/commands/login.go#L100)),
a 96-hex key is rejected outright. Verified against a freshly generated
P-384 key:

```
$ mcp-publisher login dns --domain=example.com --private-key=$KEY
Signing in process using key algorithm ed25519
Error: failed to get token: failed to sign timestamp: invalid seed length: expected 32 bytes, got 48

$ mcp-publisher login dns --algorithm ecdsap384 --domain=example.com --private-key=$KEY
Signing in process using key algorithm ecdsap384
Expected proof record:                      # works
```

`--algorithm` is registered at
[login.go:129](https://github.com/modelcontextprotocol/registry/blob/main/cmd/publisher/commands/login.go#L129)
and described correctly by the CLI's own help, but `grep -rn algorithm
docs/` had **zero** hits for it. Anyone following the ECDSA path in the
docs hit that error with nothing to tell them why.

Fixed at all four in-process ECDSA sites (2 in `authentication.mdx`, 2
in `cli/commands.md`), with a note that cloud signing providers derive
the algorithm from the key itself so the flag does not apply to them.

## 2. `mcp-publisher validate --help` was broken

```
$ mcp-publisher validate --help
Unknown command: validate
...
Use 'mcp-publisher <command> --help' for more information about a command.
```

It printed the very instruction that had just failed. `main.go`
intercepted `--help` for all subcommands and called `printCommandHelp`,
whose switch had no `validate` case — making `ValidateCommand`'s own
help block
([validate.go:120-129](https://github.com/modelcontextprotocol/registry/blob/main/cmd/publisher/commands/validate.go#L120))
unreachable.

Rather than paste a third copy of the help text, `printCommandHelp` now
returns whether it handled the command, and the caller falls through to
normal dispatch when it did not. Commands own their own `--help`, and
any future command with this shape works automatically.

| Command | Before | After |
|---|---|---|
| `validate --help` | `Unknown command`, exit 0 | prints its help, exit
0 |
| `publish --help` | works | works |
| `init -h` | works | works |
| `bogus --help` | `Unknown command`, exit 0 | `Unknown command`, **exit
1** |

## 3. CLI reference corrections

- **`--registry` is not global.** It exists only on `login`; other
commands read the URL from the stored token. `publish --registry=URL`
silently treats the flag as the `server.json` path.
`cmd/publisher/README.md` had exactly this broken example.
- **`--token` was undocumented** despite `github-actions.mdx:123`
depending on it. Now documented, noting it is `login github` only.
- **`init` takes no flags** and does not prompt; it writes `TODO:`
placeholders. The reference said `init [options]` and "Prompts for
missing required fields".
- **Refreshed the `--help` transcript** in `quickstart.mdx`, which
listed only 4 of 6 commands. Now byte-for-byte identical to real output
(diffed).
- **Added `validate`** to the command list in `cmd/publisher/README.md`.

## Verification

`go build` and `go test ./cmd/...` pass. All four help paths exercised
with exit codes checked. The 23 `goconst` findings in `validate_test.go`
are pre-existing — identical count on pristine `main`, none in
`main.go`. Quickstart transcript diffed against real output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Radoslav Dimitrov 2026-08-10 16:25:00 +03:00 • committed by GitHub
commit 43402d8f6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 45 additions and 17 deletions

View file

@ -14,9 +14,12 @@ make publisher
make dev-compose # Start local registry make dev-compose # Start local registry
./bin/mcp-publisher init ./bin/mcp-publisher init
./bin/mcp-publisher login none --registry=http://localhost:8080 ./bin/mcp-publisher login none --registry=http://localhost:8080
./bin/mcp-publisher publish --registry=http://localhost:8080 ./bin/mcp-publisher publish
``` ```
`publish` takes the registry URL from the token saved by `login`, so it accepts no `--registry`
flag — passing one would be read as the `server.json` path.
## Architecture ## Architecture
### Commands ### Commands
@ -24,6 +27,7 @@ make dev-compose # Start local registry
- **`login`** - Handle authentication (github, dns, http, none) - **`login`** - Handle authentication (github, dns, http, none)
- **`publish`** - Validate and upload servers to registry - **`publish`** - Validate and upload servers to registry
- **`status`** - Update server lifecycle status (active, deprecated, deleted) - **`status`** - Update server lifecycle status (active, deprecated, deleted)
- **`validate`** - Validate server.json without publishing
- **`logout`** - Clear stored credentials - **`logout`** - Clear stored credentials
### Authentication Providers ### Authentication Providers

View file

@ -27,10 +27,12 @@ func main() {
os.Exit(1) os.Exit(1)
} }
// Check for help flag for subcommands // Check for help flag for subcommands. Commands without an entry in
// printCommandHelp handle --help themselves, so fall through to dispatch.
if len(os.Args) >= 3 && (os.Args[2] == "--help" || os.Args[2] == "-h") { if len(os.Args) >= 3 && (os.Args[2] == "--help" || os.Args[2] == "-h") {
printCommandHelp(os.Args[1]) if printCommandHelp(os.Args[1]) {
return return
}
} }
var err error var err error
@ -81,7 +83,10 @@ func printUsage() {
_, _ = fmt.Fprintln(os.Stdout, "Use 'mcp-publisher <command> --help' for more information about a command.") _, _ = fmt.Fprintln(os.Stdout, "Use 'mcp-publisher <command> --help' for more information about a command.")
} }
func printCommandHelp(command string) { // printCommandHelp prints help for a specific command and reports whether it had
// an entry here. Commands without one parse --help themselves, so the caller
// should fall through to normal dispatch when this returns false.
func printCommandHelp(command string) bool {
switch command { switch command {
case "init": case "init":
_, _ = fmt.Fprintln(os.Stdout, "Create a server.json file template") _, _ = fmt.Fprintln(os.Stdout, "Create a server.json file template")
@ -165,7 +170,8 @@ func printCommandHelp(command string) {
_, _ = fmt.Fprintln(os.Stdout, "You must be logged in before updating status. Run 'mcp-publisher login' first.") _, _ = fmt.Fprintln(os.Stdout, "You must be logged in before updating status. Run 'mcp-publisher login' first.")
default: default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command) return false
printUsage()
} }
return true
} }

View file

@ -174,7 +174,7 @@ mcp-publisher login dns --domain "${MY_DOMAIN}" --private-key "${PRIVATE_KEY}"
MY_DOMAIN="example.com" MY_DOMAIN="example.com"
PRIVATE_KEY="$(openssl ec -in key.pem -noout -text | grep -A4 "priv:" | tail -n +2 | tr -d ' :\n')" PRIVATE_KEY="$(openssl ec -in key.pem -noout -text | grep -A4 "priv:" | tail -n +2 | tr -d ' :\n')"
mcp-publisher login dns --domain "${MY_DOMAIN}" --private-key "${PRIVATE_KEY}" mcp-publisher login dns --algorithm ecdsap384 --domain "${MY_DOMAIN}" --private-key "${PRIVATE_KEY}"
``` ```
```bash Google KMS ```bash Google KMS
@ -298,7 +298,7 @@ mcp-publisher login http --domain "${MY_DOMAIN}" --private-key "${PRIVATE_KEY}"
```bash ECDSA P-384 ```bash ECDSA P-384
MY_DOMAIN="example.com" MY_DOMAIN="example.com"
PRIVATE_KEY="$(openssl ec -in key.pem -noout -text | grep -A4 "priv:" | tail -n +2 | tr -d ' :\n')" PRIVATE_KEY="$(openssl ec -in key.pem -noout -text | grep -A4 "priv:" | tail -n +2 | tr -d ' :\n')"
mcp-publisher login http --domain "${MY_DOMAIN}" --private-key "${PRIVATE_KEY}" mcp-publisher login http --algorithm ecdsap384 --domain "${MY_DOMAIN}" --private-key "${PRIVATE_KEY}"
``` ```
```bash Google KMS ```bash Google KMS

View file

@ -135,6 +135,10 @@ Commands:
login Authenticate with the registry login Authenticate with the registry
logout Clear saved authentication logout Clear saved authentication
publish Publish server.json to the registry publish Publish server.json to the registry
status Update the status of a server version
validate Validate server.json without publishing
Use 'mcp-publisher <command> --help' for more information about a command.
``` ```
## Step 4: Create `server.json` ## Step 4: Create `server.json`

View file

@ -16,7 +16,10 @@ $ brew install mcp-publisher
All commands support: All commands support:
- `--help`, `-h` - Show command help - `--help`, `-h` - Show command help
- `--registry` - Registry URL (default: `https://registry.modelcontextprotocol.io`)
`--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 ## Commands
@ -26,14 +29,14 @@ Generate a `server.json` template with automatic detection.
**Usage:** **Usage:**
```bash ```bash
mcp-publisher init [options] mcp-publisher init
``` ```
**Behavior:** **Behavior:**
- Creates `server.json` in current directory - Creates `server.json` in current directory
- Auto-detects package managers (`package.json`, `setup.py`, etc.) - Auto-detects package managers (`package.json`, `setup.py`, etc.)
- Pre-fills fields where possible - Pre-fills fields where possible
- Prompts for missing required fields - Writes `TODO:` placeholders for fields it cannot detect — it is non-interactive and takes no flags
**Example output:** **Example output:**
```json ```json
@ -59,10 +62,13 @@ Authenticate with the registry.
#### GitHub Interactive #### GitHub Interactive
```bash ```bash
mcp-publisher login github [--registry=URL] mcp-publisher login github [--token=PAT] [--registry=URL]
``` ```
- Opens browser for GitHub OAuth flow - Opens browser for GitHub OAuth flow
- Grants access to `io.github.{username}/*` and `io.github.{org}/*` namespaces - 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.
#### GitHub OIDC (CI/CD) #### GitHub OIDC (CI/CD)
```bash ```bash
@ -83,12 +89,13 @@ Also see [the guide to publishing from GitHub Actions](../../modelcontextprotoco
#### DNS Verification #### DNS Verification
```bash ```bash
mcp-publisher login dns --domain=example.com --private-key=HEX_KEY [--registry=URL] mcp-publisher login dns --domain=example.com --private-key=HEX_KEY [--algorithm=ed25519|ecdsap384] [--registry=URL]
``` ```
- Verifies domain ownership via DNS TXT record - Verifies domain ownership via DNS TXT record
- Grants access to `com.example.*` namespaces - Grants access to `com.example.*` namespaces
- Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex) - Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex)
- The private key can be stored in a cloud signing provider like Google KMS or Azure Key Vault. - `--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) **Setup:** (for Ed25519, recommended)
```bash ```bash
@ -118,6 +125,9 @@ openssl ec -in key.pem -text -noout -conv_form compressed | grep -A4 "pub:" | ta
# Extract private key for login # Extract private key for login
openssl ec -in <pem path> -noout -text | grep -A4 "priv:" | tail -n +2 | tr -d ' :\n' 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) **Setup:** (for Google KMS signing)
@ -178,12 +188,13 @@ mcp-publisher login dns azure-key-vault --domain=example.com --vault MyKeyVault
#### HTTP Verification #### HTTP Verification
```bash ```bash
mcp-publisher login http --domain=example.com --private-key=HEX_KEY [--registry=URL] mcp-publisher login http --domain=example.com --private-key=HEX_KEY [--algorithm=ed25519|ecdsap384] [--registry=URL]
``` ```
- Verifies domain ownership via HTTPS endpoint - Verifies domain ownership via HTTPS endpoint
- Grants access to `com.example.*` namespaces - Grants access to `com.example.*` namespaces
- Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex) - Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex)
- The private key can be stored in a cloud signing provider like Google KMS or Azure Key Vault. - `--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) **Setup:** (for Ed25519, recommended)
```bash ```bash
@ -203,6 +214,9 @@ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out key.pem
# Host public key at: # Host public key at:
# https://example.com/.well-known/mcp-registry-auth # https://example.com/.well-known/mcp-registry-auth
# Content: v=MCPv1; k=ecdsap384; p=PUBLIC_KEY # 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`. Cloud signing is also supported for HTTP authentication, similar to the DNS examples above. Just swap out the `dns` positional argument for `http`.