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
./bin/mcp-publisher init
./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
### Commands
@ -24,6 +27,7 @@ make dev-compose # Start local registry
- **`login`** - Handle authentication (github, dns, http, none)
- **`publish`** - Validate and upload servers to registry
- **`status`** - Update server lifecycle status (active, deprecated, deleted)
- **`validate`** - Validate server.json without publishing
- **`logout`** - Clear stored credentials
### Authentication Providers

View file

@ -27,11 +27,13 @@ func main() {
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") {
printCommandHelp(os.Args[1])
if printCommandHelp(os.Args[1]) {
return
}
}
var err error
switch os.Args[1] {
@ -81,7 +83,10 @@ func printUsage() {
_, _ = 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 {
case "init":
_, _ = 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.")
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
printUsage()
return false
}
return true
}

View file

@ -174,7 +174,7 @@ mcp-publisher login dns --domain "${MY_DOMAIN}" --private-key "${PRIVATE_KEY}"
MY_DOMAIN="example.com"
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
@ -298,7 +298,7 @@ mcp-publisher login http --domain "${MY_DOMAIN}" --private-key "${PRIVATE_KEY}"
```bash ECDSA P-384
MY_DOMAIN="example.com"
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

View file

@ -135,6 +135,10 @@ Commands:
login Authenticate with the registry
logout Clear saved authentication
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`

View file

@ -16,7 +16,10 @@ $ brew install mcp-publisher
All commands support:
- `--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
@ -26,14 +29,14 @@ Generate a `server.json` template with automatic detection.
**Usage:**
```bash
mcp-publisher init [options]
mcp-publisher init
```
**Behavior:**
- Creates `server.json` in current directory
- Auto-detects package managers (`package.json`, `setup.py`, etc.)
- 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:**
```json
@ -59,10 +62,13 @@ Authenticate with the registry.
#### GitHub Interactive
```bash
mcp-publisher login github [--registry=URL]
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.
#### GitHub OIDC (CI/CD)
```bash
@ -83,12 +89,13 @@ Also see [the guide to publishing from GitHub Actions](../../modelcontextprotoco
#### DNS Verification
```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
- Grants access to `com.example.*` namespaces
- 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)
```bash
@ -118,6 +125,9 @@ openssl ec -in key.pem -text -noout -conv_form compressed | grep -A4 "pub:" | ta
# 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)
@ -178,12 +188,13 @@ mcp-publisher login dns azure-key-vault --domain=example.com --vault MyKeyVault
#### HTTP Verification
```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
- Grants access to `com.example.*` namespaces
- 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)
```bash
@ -203,6 +214,9 @@ 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`.