## Summary DNS auth users have repeatedly hit two failure modes that both produce the same opaque "Ed25519 signature verification failed" error, making them indistinguishable to debug. This PR makes both diagnosable end-to-end without changing the auth protocol or any successful path. The two recurring failure modes: 1. **Wrong placement** — TXT record put under a selector (`_mcp-auth.<domain>` or `_mcp-registry.<domain>`) instead of the apex. DKIM-style intuition, but MCP DNS auth uses SPF-style apex placement. See #385 (PR #388 fixed the docs but the failure recurred), #1103, #1126. 2. **Stale apex record** — old TXT record left behind after a key rotation, silently tried first and rejected. ## Changes **`internal/api/handlers/v0/auth/dns.go`** — when the apex has no `v=MCPv1` record, probe `_mcp-auth.<domain>` and `_mcp-registry.<domain>` in parallel (2s individual timeout). If a record is found there, return a targeted error pointing the user at the apex. Costs zero extra DNS work in the success path. **`internal/api/handlers/v0/auth/common.go`** — when verification fails against records that *are* present, include short fingerprints of every key the registry tried (`ed25519:YJLsHFhu`-style 8-char prefix). A new `ErrSignatureMismatch` sentinel keeps structural errors (wrong signature size, unsupported algorithm) passing through unchanged via `errors.Is`, so existing assertions on those messages still hold. **`docs/modelcontextprotocol-io/authentication.mdx`** — anti-pattern `<Warning>` callout in the DNS section explicitly warning against selector placement and stale records. **Tests** — two new tests: - `TestDNSAuthHandler_WrongSelectorProbe` — verifies the probe surfaces records found at `_mcp-auth.` / `_mcp-registry.` and falls through to the standard error when nothing is found anywhere - `TestDNSAuthHandler_StaleKeyFingerprintInError` — verifies the fingerprint hint appears in the failure message for #1126's exact scenario One existing test assertion was updated from a generic `"signature verification failed"` substring to the more specific `"invalid signature size for ECDSA P-384"` it should always have been asserting; the new sentinel-based logic surfaces structural errors verbatim. ## Test plan - [x] `make lint` — clean - [x] `make test-unit` — all auth tests pass (existing + 2 new) - [x] Verified existing single-key and multi-key paths preserve their original (more specific) error messages where they were already specific - [ ] Reviewer to sanity-check the docs callout renders well in the Mintlify theme ## Out of scope The publisher-side preflight DNS check (cross-link in #845) is intentionally **not** in this PR — that belongs in the broader CLI UX work tracked by #845. Refs #845, #385, #1103 Fixes #1126 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
300 lines
11 KiB
Text
300 lines
11 KiB
Text
---
|
|
title: How to Authenticate When Publishing to the Official MCP Registry
|
|
sidebarTitle: Authentication
|
|
---
|
|
|
|
<Note>
|
|
The MCP Registry is currently in preview. Breaking changes or data resets may occur before general availability. If you encounter any issues, please report them on [GitHub](https://github.com/modelcontextprotocol/registry/issues).
|
|
</Note>
|
|
|
|
You must authenticate before publishing to the official MCP Registry. The MCP Registry supports different authentication methods. Which authentication method you choose determines the namespace of your server's name.
|
|
|
|
If you choose GitHub-based authentication, your server's name in `server.json` **MUST** be of the form `io.github.username/*` (or `io.github.orgname/*`). For example, `io.github.alice/weather-server`.
|
|
|
|
If you choose domain-based authentication, your server's name in `server.json` **MUST** be of the form `com.example.*/*`, where `com.example` is the reverse-DNS form of your domain name. For example, `io.modelcontextprotocol/everything`.
|
|
|
|
| Authentication | Name Format | Example Name |
|
|
| -------------- | ----------------------------------------------- | ------------------------------------ |
|
|
| GitHub-based | `io.github.username/*` or `io.github.orgname/*` | `io.github.alice/weather-server` |
|
|
| domain-based | `com.example.*/*` | `io.modelcontextprotocol/everything` |
|
|
|
|
## GitHub Authentication
|
|
|
|
GitHub authentication uses an OAuth flow initiated by the `mcp-publisher` CLI tool.
|
|
|
|
To perform GitHub authentication, navigate to your server project directory and run:
|
|
|
|
```bash
|
|
mcp-publisher login github
|
|
```
|
|
|
|
You should see output like:
|
|
|
|
```text Output
|
|
Logging in with github...
|
|
|
|
To authenticate, please:
|
|
1. Go to: https://github.com/login/device
|
|
2. Enter code: ABCD-1234
|
|
3. Authorize this application
|
|
Waiting for authorization...
|
|
```
|
|
|
|
Visit the link, follow the prompts, and enter the authorization code that was printed in the terminal (e.g., `ABCD-1234` in the above output). Once complete, go back to the terminal, and you should see output like:
|
|
|
|
```text Output
|
|
Successfully authenticated!
|
|
✓ Successfully logged in
|
|
```
|
|
|
|
## DNS Authentication
|
|
|
|
DNS authentication is a domain-based authentication method that relies on a DNS TXT record.
|
|
|
|
<Warning>
|
|
The TXT record must be placed on the **apex** of your domain (e.g. `example.com`), **not** under a selector like `_mcp-auth.example.com` or `_mcp-registry.example.com`. MCP DNS auth follows SPF-style placement (apex), not DKIM-style (selector). If you put the record under a selector, the registry will not see it and authentication will fail with a generic signature error.
|
|
|
|
If you rotate keys, also remember to remove the previous TXT record from the apex — a stale record left behind will be tried first and cause verification to fail.
|
|
</Warning>
|
|
|
|
To perform DNS authentication using the `mcp-publisher` CLI tool, run the following commands in your server project directory to generate a TXT record based on a public/private key pair:
|
|
|
|
<CodeGroup>
|
|
|
|
```bash Ed25519
|
|
MY_DOMAIN="example.com"
|
|
|
|
# Generate public/private key pair using Ed25519
|
|
openssl genpkey -algorithm Ed25519 -out key.pem
|
|
|
|
# Generate TXT record
|
|
PUBLIC_KEY="$(openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64)"
|
|
echo "${MY_DOMAIN}. IN TXT \"v=MCPv1; k=ed25519; p=${PUBLIC_KEY}\""
|
|
```
|
|
|
|
```bash ECDSA P-384
|
|
MY_DOMAIN="example.com"
|
|
|
|
# Generate public/private key pair using ECDSA P-384
|
|
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out key.pem
|
|
|
|
# Generate TXT record
|
|
PUBLIC_KEY="$(openssl ec -in key.pem -text -noout -conv_form compressed | grep -A4 "pub:" | tail -n +2 | tr -d ' :\n' | xxd -r -p | base64)"
|
|
echo "${MY_DOMAIN}. IN TXT \"v=MCPv1; k=ecdsap384; p=${PUBLIC_KEY}\""
|
|
```
|
|
|
|
```bash Google KMS
|
|
MY_DOMAIN="example.com"
|
|
MY_PROJECT="myproject"
|
|
MY_KEYRING="mykeyring"
|
|
MY_KEY_NAME="mykey"
|
|
|
|
# Log in using gcloud CLI (https://cloud.google.com/sdk/docs/install)
|
|
gcloud auth login
|
|
|
|
# Set default project
|
|
gcloud config set project "${MY_PROJECT}"
|
|
|
|
# Create a keyring in your project
|
|
gcloud kms keyrings create "${MY_KEYRING}" --location global
|
|
|
|
# Create an Ed25519 signing key
|
|
gcloud kms keys create "${MY_KEY_NAME}" --default-algorithm=ec-sign-ed25519 --purpose=asymmetric-signing --keyring="${MY_KEYRING}" --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="${MY_DOMAIN}" --resource="projects/${MY_PROJECT}/locations/global/keyRings/${MY_KEYRING}/cryptoKeys/${MY_KEY_NAME}/cryptoKeyVersions/1"
|
|
|
|
# Copy the "Expected proof record":
|
|
# ${MY_DOMAIN}. IN TXT "v=MCPv1; k=ed25519; p=${PUBLIC_KEY}"
|
|
```
|
|
|
|
```bash Azure Key Vault
|
|
MY_DOMAIN="example.com"
|
|
MY_SUBSCRIPTION="subscription name or ID"
|
|
MY_RESOURCE_GROUP="MyResourceGroup"
|
|
MY_KEY_VAULT="MyKeyVault"
|
|
MY_KEY_NAME="MyKey"
|
|
|
|
# Log in using Azure CLI (https://learn.microsoft.com/en-us/cli/azure/install-azure-cli)
|
|
az login
|
|
|
|
# Set default subscription
|
|
az account set --subscription "${MY_SUBSCRIPTION}"
|
|
|
|
# Create a resource group
|
|
az group create --location westus --resource-group "${MY_RESOURCE_GROUP}"
|
|
|
|
# Create a key vault
|
|
az keyvault create --name "${MY_KEY_VAULT}" --location westus --resource-group "${MY_RESOURCE_GROUP}"
|
|
|
|
# Create an ECDSA P-384 signing key
|
|
az keyvault key create --name "${MY_KEY_NAME}" --vault-name "${MY_KEY_VAULT}" --curve P-384
|
|
|
|
# Attempt login to show the public key
|
|
mcp-publisher login dns azure-key-vault --domain="${MY_DOMAIN}" --vault "${MY_KEY_VAULT}" --key "${MY_KEY_NAME}"
|
|
|
|
# Copy the "Expected proof record":
|
|
# ${MY_DOMAIN}. IN TXT "v=MCPv1; k=ecdsap384; p=${PUBLIC_KEY}"
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
Then add the TXT record using your DNS provider's control panel. It may take several minutes for the TXT record to propagate. After the TXT record has propagated, log in using the `mcp-publisher login` command:
|
|
|
|
<CodeGroup>
|
|
|
|
```bash Ed25519
|
|
MY_DOMAIN="example.com"
|
|
|
|
PRIVATE_KEY="$(openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')"
|
|
mcp-publisher login dns --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 dns --domain "${MY_DOMAIN}" --private-key "${PRIVATE_KEY}"
|
|
```
|
|
|
|
```bash Google KMS
|
|
MY_DOMAIN="example.com"
|
|
MY_PROJECT="myproject"
|
|
MY_KEYRING="mykeyring"
|
|
MY_KEY_NAME="mykey"
|
|
|
|
mcp-publisher login dns google-kms --domain="${MY_DOMAIN}" --resource="projects/${MY_PROJECT}/locations/global/keyRings/${MY_KEYRING}/cryptoKeys/${MY_KEY_NAME}/cryptoKeyVersions/1"
|
|
```
|
|
|
|
```bash Azure Key Vault
|
|
MY_DOMAIN="example.com"
|
|
MY_KEY_VAULT="MyKeyVault"
|
|
MY_KEY_NAME="MyKey"
|
|
|
|
mcp-publisher login dns azure-key-vault --domain="${MY_DOMAIN}" --vault "${MY_KEY_VAULT}" --key "${MY_KEY_NAME}"
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
## HTTP Authentication
|
|
|
|
HTTP authentication is a domain-based authentication method that relies on a `/.well-known/mcp-registry-auth` file hosted on your domain. For example, `https://example.com/.well-known/mcp-registry-auth`.
|
|
|
|
To perform HTTP authentication using the `mcp-publisher` CLI tool, run the following commands in your server project directory to generate an `mcp-registry-auth` file based on a public/private key pair:
|
|
|
|
<CodeGroup>
|
|
|
|
```bash Ed25519
|
|
# Generate public/private key pair using Ed25519
|
|
openssl genpkey -algorithm Ed25519 -out key.pem
|
|
|
|
# Generate mcp-registry-auth file
|
|
PUBLIC_KEY="$(openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64)"
|
|
echo "v=MCPv1; k=ed25519; p=${PUBLIC_KEY}" > mcp-registry-auth
|
|
```
|
|
|
|
```bash ECDSA P-384
|
|
# Generate public/private key pair using ECDSA P-384
|
|
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out key.pem
|
|
|
|
# Generate mcp-registry-auth file
|
|
PUBLIC_KEY="$(openssl ec -in key.pem -text -noout -conv_form compressed | grep -A4 "pub:" | tail -n +2 | tr -d ' :\n' | xxd -r -p | base64)"
|
|
echo "v=MCPv1; k=ecdsap384; p=${PUBLIC_KEY}" > mcp-registry-auth
|
|
```
|
|
|
|
```bash Google KMS
|
|
MY_DOMAIN="example.com"
|
|
MY_PROJECT="myproject"
|
|
MY_KEYRING="mykeyring"
|
|
MY_KEY_NAME="mykey"
|
|
|
|
# Log in using gcloud CLI (https://cloud.google.com/sdk/docs/install)
|
|
gcloud auth login
|
|
|
|
# Set default project
|
|
gcloud config set project "${MY_PROJECT}"
|
|
|
|
# Create a keyring in your project
|
|
gcloud kms keyrings create "${MY_KEYRING}" --location global
|
|
|
|
# Create an Ed25519 signing key
|
|
gcloud kms keys create "${MY_KEY_NAME}" --default-algorithm=ec-sign-ed25519 --purpose=asymmetric-signing --keyring="${MY_KEYRING}" --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 http google-kms --domain="${MY_DOMAIN}" --resource="projects/${MY_PROJECT}/locations/global/keyRings/${MY_KEYRING}/cryptoKeys/${MY_KEY_NAME}/cryptoKeyVersions/1"
|
|
|
|
# Copy the "Expected proof record" to `./mcp-registry-auth`:
|
|
# v=MCPv1; k=ed25519; p=${PUBLIC_KEY}
|
|
```
|
|
|
|
```bash Azure Key Vault
|
|
MY_DOMAIN="example.com"
|
|
MY_SUBSCRIPTION="subscription name or ID"
|
|
MY_RESOURCE_GROUP="MyResourceGroup"
|
|
MY_KEY_VAULT="MyKeyVault"
|
|
MY_KEY_NAME="MyKey"
|
|
|
|
# Log in using Azure CLI (https://learn.microsoft.com/en-us/cli/azure/install-azure-cli)
|
|
az login
|
|
|
|
# Set default subscription
|
|
az account set --subscription "${MY_SUBSCRIPTION}"
|
|
|
|
# Create a resource group
|
|
az group create --location westus --resource-group "${MY_RESOURCE_GROUP}"
|
|
|
|
# Create a key vault
|
|
az keyvault create --name "${MY_KEY_VAULT}" --location westus --resource-group "${MY_RESOURCE_GROUP}"
|
|
|
|
# Create an ECDSA P-384 signing key
|
|
az keyvault key create --name "${MY_KEY_NAME}" --vault-name "${MY_KEY_VAULT}" --curve P-384
|
|
|
|
# Attempt login to show the public key
|
|
mcp-publisher login http azure-key-vault --domain="${MY_DOMAIN}" --vault "${MY_KEY_VAULT}" --key "${MY_KEY_NAME}"
|
|
|
|
# Copy the "Expected proof record" to `./mcp-registry-auth`:
|
|
# v=MCPv1; k=ecdsap384; p=${PUBLIC_KEY}
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
Then host the `mcp-registry-auth` file at `/.well-known/mcp-registry-auth` on your domain. After the file is hosted, log in using the `mcp-publisher login` command:
|
|
|
|
<CodeGroup>
|
|
|
|
```bash Ed25519
|
|
MY_DOMAIN="example.com"
|
|
PRIVATE_KEY="$(openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')"
|
|
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}"
|
|
```
|
|
|
|
```bash Google KMS
|
|
MY_DOMAIN="example.com"
|
|
MY_PROJECT="myproject"
|
|
MY_KEYRING="mykeyring"
|
|
MY_KEY_NAME="mykey"
|
|
|
|
mcp-publisher login http google-kms --domain="${MY_DOMAIN}" --resource="projects/${MY_PROJECT}/locations/global/keyRings/${MY_KEYRING}/cryptoKeys/${MY_KEY_NAME}/cryptoKeyVersions/1"
|
|
```
|
|
|
|
```bash Azure Key Vault
|
|
MY_DOMAIN="example.com"
|
|
MY_KEY_VAULT="MyKeyVault"
|
|
MY_KEY_NAME="MyKey"
|
|
|
|
mcp-publisher login http azure-key-vault --domain="${MY_DOMAIN}" --vault "${MY_KEY_VAULT}" --key "${MY_KEY_NAME}"
|
|
```
|
|
|
|
</CodeGroup>
|