Skip to content

Commit 31a5740

Browse files
authored
Merge branch 'main' into allow-google-artifact-registry
2 parents ae78b7b + 4805f5c commit 31a5740

File tree

13 files changed

+1528
-78
lines changed

13 files changed

+1528
-78
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
cache: true
2626

2727
- name: Install cosign
28-
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad
28+
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5
2929

3030
- name: Install Syft
3131
uses: anchore/sbom-action/[email protected]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The MCP registry provides MCP clients with a list of MCP servers, like an app st
66

77
## Development Status
88

9+
**2025-10-24 update**: The Registry API has entered an **API freeze (v0.1)** 🎉. For the next month or more, the API will remain stable with no breaking changes, allowing integrators to confidently implement support. This freeze applies to v0.1 while development continues on v0. We'll use this period to validate the API in real-world integrations and gather feedback to shape v1 for general availability. Thank you to everyone for your contributions and patience—your involvement has been key to getting us here!
10+
911
**2025-09-08 update**: The registry has launched in preview 🎉 ([announcement blog post](https://blog.modelcontextprotocol.io/posts/2025-09-08-mcp-registry-preview/)). While the system is now more stable, this is still a preview release and breaking changes or data resets may occur. A general availability (GA) release will follow later. We'd love your feedback in [GitHub discussions](https://github.com/modelcontextprotocol/registry/discussions/new?category=ideas) or in the [#registry-dev Discord](https://discord.com/channels/1358869848138059966/1369487942862504016) ([joining details here](https://modelcontextprotocol.io/community/communication)).
1012

1113
Current key maintainers:

cmd/publisher/auth/common.go

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,38 @@ package auth
33
import (
44
"bytes"
55
"context"
6+
"crypto/ecdsa"
67
"crypto/ed25519"
8+
"crypto/elliptic"
9+
"crypto/rand"
10+
"crypto/sha512"
711
"encoding/hex"
812
"encoding/json"
913
"fmt"
1014
"io"
15+
"math/big"
1116
"net/http"
1217
"time"
1318
)
1419

20+
type CryptoAlgorithm string
21+
22+
const (
23+
AlgorithmEd25519 CryptoAlgorithm = "ed25519"
24+
25+
// ECDSA with NIST P-384 curve
26+
// public key is in compressed format
27+
// signature is in R || S format
28+
AlgorithmECDSAP384 CryptoAlgorithm = "ecdsap384"
29+
)
30+
1531
// CryptoProvider provides common functionality for DNS and HTTP authentication
1632
type CryptoProvider struct {
17-
registryURL string
18-
domain string
19-
hexSeed string
20-
authMethod string
33+
registryURL string
34+
domain string
35+
privateKey string
36+
cryptoAlgorithm CryptoAlgorithm
37+
authMethod string
2138
}
2239

2340
// GetToken retrieves the registry JWT token using cryptographic authentication
@@ -26,38 +43,100 @@ func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
2643
return "", fmt.Errorf("%s domain is required", c.authMethod)
2744
}
2845

29-
if c.hexSeed == "" {
30-
return "", fmt.Errorf("%s private key (hex seed) is required", c.authMethod)
46+
if c.privateKey == "" {
47+
return "", fmt.Errorf("%s private key (hex) is required", c.authMethod)
3148
}
3249

33-
// Decode hex seed to private key
34-
seedBytes, err := hex.DecodeString(c.hexSeed)
50+
// Decode private key from hex
51+
privateKeyBytes, err := hex.DecodeString(c.privateKey)
3552
if err != nil {
36-
return "", fmt.Errorf("invalid hex seed format: %w", err)
37-
}
38-
39-
if len(seedBytes) != ed25519.SeedSize {
40-
return "", fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(seedBytes))
53+
return "", fmt.Errorf("invalid hex private key format: %w", err)
4154
}
4255

43-
privateKey := ed25519.NewKeyFromSeed(seedBytes)
44-
4556
// Generate current timestamp
4657
timestamp := time.Now().UTC().Format(time.RFC3339)
47-
48-
// Sign the timestamp
49-
signature := ed25519.Sign(privateKey, []byte(timestamp))
50-
signedTimestamp := hex.EncodeToString(signature)
58+
signedTimestamp, err := c.signMessage(privateKeyBytes, []byte(timestamp))
59+
if err != nil {
60+
return "", fmt.Errorf("failed to sign timestamp: %w", err)
61+
}
62+
signedTimestampHex := hex.EncodeToString(signedTimestamp)
5163

5264
// Exchange signature for registry token
53-
registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, timestamp, signedTimestamp)
65+
registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, timestamp, signedTimestampHex)
5466
if err != nil {
5567
return "", fmt.Errorf("failed to exchange %s signature: %w", c.authMethod, err)
5668
}
5769

5870
return registryToken, nil
5971
}
6072

73+
func (c *CryptoProvider) signMessage(privateKeyBytes []byte, message []byte) ([]byte, error) {
74+
switch c.cryptoAlgorithm {
75+
case AlgorithmEd25519:
76+
if len(privateKeyBytes) != ed25519.SeedSize {
77+
return nil, fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(privateKeyBytes))
78+
}
79+
80+
privateKey := ed25519.NewKeyFromSeed(privateKeyBytes)
81+
signature := ed25519.Sign(privateKey, message)
82+
return signature, nil
83+
case AlgorithmECDSAP384:
84+
if len(privateKeyBytes) != 48 {
85+
return nil, fmt.Errorf("invalid seed length for ECDSA P-384: expected 48 bytes, got %d", len(privateKeyBytes))
86+
}
87+
88+
digest := sha512.Sum384(message)
89+
curve := elliptic.P384()
90+
91+
// Parse the raw private key (compatible with Go 1.24)
92+
privateKey, err := parseRawPrivateKey(curve, privateKeyBytes)
93+
if err != nil {
94+
return nil, fmt.Errorf("failed to parse ECDSA private key: %w", err)
95+
}
96+
97+
r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:])
98+
if err != nil {
99+
return nil, fmt.Errorf("failed to sign message: %w", err)
100+
}
101+
signature := append(r.Bytes(), s.Bytes()...)
102+
return signature, nil
103+
default:
104+
return nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm)
105+
}
106+
}
107+
108+
// parseRawPrivateKey parses a raw ECDSA private key from bytes.
109+
// This mimics crypto/ecdsa.ParseRawPrivateKey from Go 1.25+ for compatibility with Go 1.24.
110+
func parseRawPrivateKey(curve elliptic.Curve, privateKeyBytes []byte) (*ecdsa.PrivateKey, error) {
111+
if curve == nil {
112+
return nil, fmt.Errorf("nil curve")
113+
}
114+
115+
// Only standard NIST curves supported
116+
switch curve {
117+
case elliptic.P224(), elliptic.P256(), elliptic.P384(), elliptic.P521():
118+
// ok
119+
default:
120+
return nil, fmt.Errorf("unsupported curve")
121+
}
122+
123+
d := new(big.Int).SetBytes(privateKeyBytes)
124+
params := curve.Params()
125+
if d.Sign() <= 0 || d.Cmp(params.N) >= 0 {
126+
return nil, fmt.Errorf("invalid private scalar")
127+
}
128+
129+
x, y := curve.ScalarBaseMult(d.Bytes())
130+
return &ecdsa.PrivateKey{
131+
PublicKey: ecdsa.PublicKey{
132+
Curve: curve,
133+
X: x,
134+
Y: y,
135+
},
136+
D: d,
137+
}, nil
138+
}
139+
61140
// NeedsLogin always returns false for cryptographic auth since no interactive login is needed
62141
func (c *CryptoProvider) NeedsLogin() bool {
63142
return false

cmd/publisher/auth/dns.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ type DNSProvider struct {
55
}
66

77
// NewDNSProvider creates a new DNS-based auth provider
8-
func NewDNSProvider(registryURL, domain, hexSeed string) Provider {
8+
func NewDNSProvider(registryURL, domain, privateKey string, cryptoAlgorithm CryptoAlgorithm) Provider {
99
return &DNSProvider{
1010
CryptoProvider: &CryptoProvider{
11-
registryURL: registryURL,
12-
domain: domain,
13-
hexSeed: hexSeed,
14-
authMethod: "dns",
11+
registryURL: registryURL,
12+
domain: domain,
13+
privateKey: privateKey,
14+
cryptoAlgorithm: cryptoAlgorithm,
15+
authMethod: "dns",
1516
},
1617
}
1718
}

cmd/publisher/auth/http.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ type HTTPProvider struct {
55
}
66

77
// NewHTTPProvider creates a new HTTP-based auth provider
8-
func NewHTTPProvider(registryURL, domain, hexSeed string) Provider {
8+
func NewHTTPProvider(registryURL, domain, privateKey string, cryptoAlgorithm CryptoAlgorithm) Provider {
99
return &HTTPProvider{
1010
CryptoProvider: &CryptoProvider{
11-
registryURL: registryURL,
12-
domain: domain,
13-
hexSeed: hexSeed,
14-
authMethod: "http",
11+
registryURL: registryURL,
12+
domain: domain,
13+
privateKey: privateKey,
14+
cryptoAlgorithm: cryptoAlgorithm,
15+
authMethod: "http",
1516
},
1617
}
1718
}

cmd/publisher/commands/login.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ const (
1717
TokenFileName = ".mcp_publisher_token" //nolint:gosec // Not a credential, just a filename
1818
)
1919

20+
type CryptoAlgorithm auth.CryptoAlgorithm
21+
22+
func (c *CryptoAlgorithm) String() string {
23+
return string(*c)
24+
}
25+
26+
func (c *CryptoAlgorithm) Set(v string) error {
27+
switch v {
28+
case string(auth.AlgorithmEd25519), string(auth.AlgorithmECDSAP384):
29+
*c = CryptoAlgorithm(v)
30+
return nil
31+
}
32+
return fmt.Errorf("invalid algorithm: %q (allowed: ed25519, ecdsap384)", v)
33+
}
34+
2035
func LoginCommand(args []string) error {
2136
if len(args) < 1 {
2237
return errors.New("authentication method required\n\nUsage: mcp-publisher login <method>\n\nMethods:\n github Interactive GitHub authentication\n github-oidc GitHub Actions OIDC authentication\n dns DNS-based authentication (requires --domain and --private-key)\n http HTTP-based authentication (requires --domain and --private-key)\n none Anonymous authentication (for testing)")
@@ -28,13 +43,15 @@ func LoginCommand(args []string) error {
2843
loginFlags := flag.NewFlagSet("login", flag.ExitOnError)
2944
var domain string
3045
var privateKey string
46+
var cryptoAlgorithm = CryptoAlgorithm(auth.AlgorithmEd25519)
3147
var registryURL string
3248

3349
loginFlags.StringVar(&registryURL, "registry", DefaultRegistryURL, "Registry URL")
3450

3551
if method == "dns" || method == "http" {
3652
loginFlags.StringVar(&domain, "domain", "", "Domain name")
37-
loginFlags.StringVar(&privateKey, "private-key", "", "Private key (64-char hex)")
53+
loginFlags.StringVar(&privateKey, "private-key", "", "Private key (hex)")
54+
loginFlags.Var(&cryptoAlgorithm, "algorithm", "Cryptographic algorithm (ed25519, ecdsap384)")
3855
}
3956

4057
if err := loginFlags.Parse(args[1:]); err != nil {
@@ -52,12 +69,12 @@ func LoginCommand(args []string) error {
5269
if domain == "" || privateKey == "" {
5370
return errors.New("dns authentication requires --domain and --private-key")
5471
}
55-
authProvider = auth.NewDNSProvider(registryURL, domain, privateKey)
72+
authProvider = auth.NewDNSProvider(registryURL, domain, privateKey, auth.CryptoAlgorithm(cryptoAlgorithm))
5673
case "http":
5774
if domain == "" || privateKey == "" {
5875
return errors.New("http authentication requires --domain and --private-key")
5976
}
60-
authProvider = auth.NewHTTPProvider(registryURL, domain, privateKey)
77+
authProvider = auth.NewHTTPProvider(registryURL, domain, privateKey, auth.CryptoAlgorithm(cryptoAlgorithm))
6178
case "none":
6279
authProvider = auth.NewNoneProvider(registryURL)
6380
default:

docs/guides/publishing/publish-server.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,8 @@ echo "yourcompany.com. IN TXT \"v=MCPv1; k=ed25519; p=$(openssl pkey -in key.pem
471471
mcp-publisher login dns --domain yourcompany.com --private-key $(openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')
472472
```
473473

474+
The ECDSA P-384 crypto algorithm is also supported, along with HTTP-based authenication. See the [publisher CLI commands reference](../../reference/cli/commands.md) for more details.
475+
474476
## Step 5: Publish Your Server
475477

476478
With authentication complete, publish your server:

docs/reference/cli/commands.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ mcp-publisher login dns --domain=example.com --private-key=HEX_KEY [--registry=U
8080
```
8181
- Verifies domain ownership via DNS TXT record
8282
- Grants access to `com.example.*` namespaces
83-
- Requires Ed25519 private key (64-character hex)
83+
- Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex)
8484

85-
**Setup:**
85+
**Setup:** (for Ed25519, recommended)
8686
```bash
8787
# Generate keypair
8888
openssl genpkey -algorithm Ed25519 -out key.pem
@@ -97,15 +97,30 @@ openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64
9797
openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n'
9898
```
9999

100+
**Setup:** (for ECDSA P-384)
101+
```bash
102+
# Generate keypair
103+
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out key.pem
104+
105+
# Get public key for DNS record
106+
openssl ec -in key.pem -text -noout -conv_form compressed | grep -A4 "pub:" | tail -n +2 | tr -d ' :\n' | xxd -r -p | base64
107+
108+
# Add DNS TXT record:
109+
# example.com. IN TXT "v=MCPv1; k=ecdsap384; p=PUBLIC_KEY"
110+
111+
# Extract private key for login
112+
openssl ec -in <pem path> -noout -text | grep -A4 "priv:" | tail -n +2 | tr -d ' :\n'
113+
```
114+
100115
#### HTTP Verification
101116
```bash
102117
mcp-publisher login http --domain=example.com --private-key=HEX_KEY [--registry=URL]
103118
```
104119
- Verifies domain ownership via HTTPS endpoint
105120
- Grants access to `com.example.*` namespaces
106-
- Requires Ed25519 private key (64-character hex)
121+
- Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex)
107122

108-
**Setup:**
123+
**Setup:** (for Ed25519, recommended)
109124
```bash
110125
# Generate keypair (same as DNS)
111126
openssl genpkey -algorithm Ed25519 -out key.pem
@@ -115,6 +130,16 @@ openssl genpkey -algorithm Ed25519 -out key.pem
115130
# Content: v=MCPv1; k=ed25519; p=PUBLIC_KEY
116131
```
117132

133+
**Setup:** (for ECDSA P-384)
134+
```bash
135+
# Generate keypair (same as DNS)
136+
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out key.pem
137+
138+
# Host public key at:
139+
# https://example.com/.well-known/mcp-registry-auth
140+
# Content: v=MCPv1; k=ecdsap384; p=PUBLIC_KEY
141+
```
142+
118143
#### Anonymous (Testing)
119144
```bash
120145
mcp-publisher login none [--registry=URL]

docs/reference/server-json/generic-server-json.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ The schema contains all field definitions, validation rules, examples, and detai
1414

1515
The official registry has some more restrictions on top of this. See the [official registry requirements](./official-registry-requirements.md) for details.
1616

17+
## Extension Metadata with `_meta`
18+
19+
The optional `_meta` field allows publishers to include custom metadata alongside their server definitions using reverse DNS namespacing.
20+
21+
```jsonc
22+
{
23+
"_meta": {
24+
"io.modelcontextprotocol.registry/publisher-provided": {
25+
// Your custom metadata here
26+
}
27+
}
28+
}
29+
```
30+
31+
When publishing to the official registry, custom metadata must be placed under the key `io.modelcontextprotocol.registry/publisher-provided`. See the [official registry requirements](./official-registry-requirements.md) for detailed restrictions and examples.
32+
1733
## Examples
1834

1935
<!-- As a heads up, these are used as part of tests/integration/main.go -->

0 commit comments

Comments
 (0)