Publish Auth Interface (#112)

This pull request introduces significant updates to the authentication
system for the MCP Registry Publisher Tool. The changes focus on
transitioning to an interface-based authentication system, implementing
GitHub OAuth device flow as the default authentication method, and
restructuring the codebase for better modularity and extensibility.

Adding an interface to the publishing CLI, so that it'll be easier to
add more auth methods as they become available.
This commit is contained in:
Avinash Sridhar 2025-05-30 15:05:15 -04:00 • committed by GitHub
commit a4cefcf05f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 465 additions and 266 deletions

View file

@ -1,6 +1,6 @@
# MCP Registry Publisher Tool
The MCP Registry Publisher Tool is designed to publish Model Context Protocol (MCP) server details to an MCP registry. This tool currently only handles GitHub authentication via device flow and manages the publishing process.
The MCP Registry Publisher Tool is designed to publish Model Context Protocol (MCP) server details to an MCP registry. This tool uses GitHub OAuth device flow authentication to securely manage the publishing process.
## Building the Tool
@ -20,29 +20,30 @@ The compiled binary will be placed in the `bin` directory.
```bash
# Basic usage
./bin/mcp-publisher --registry-url <REGISTRY_URL> --mcp-file <PATH_TO_MCP_FILE>
./bin/mcp-publisher -registry-url <REGISTRY_URL> -mcp-file <PATH_TO_MCP_FILE>
# Force a new login even if a token exists
./bin/mcp-publisher --registry-url <REGISTRY_URL> --mcp-file <PATH_TO_MCP_FILE> --login
./bin/mcp-publisher -registry-url <REGISTRY_URL> -mcp-file <PATH_TO_MCP_FILE> -login
```
### Command-line Arguments
- `--registry-url`: URL of the MCP registry (required)
- `--mcp-file`: Path to the MCP configuration file (required)
- `--login`: Force a new GitHub authentication even if a token already exists (overwrites existing token file)
- `--token`: Use the provided token instead of GitHub authentication (bypasses the device flow)
- `-registry-url`: URL of the MCP registry (required)
- `-mcp-file`: Path to the MCP configuration file (required)
- `-login`: Force a new GitHub authentication even if a token already exists (overwrites existing token file)
- `-auth-method`: Authentication method to use (default: github-oauth)
## Authentication
The tool uses GitHub device flow authentication:
1. The tool automatically retrieves the GitHub Client ID from the registry's health endpoint
2. When first run (or with `--login` flag), the tool will initiate the GitHub device flow
3. You'll be provided with a URL and a code to enter
4. After successful authentication, the tool saves the token locally for future use
5. The token is sent in the HTTP Authorization header with the Bearer scheme
The tool has been simplified to use **GitHub OAuth device flow authentication exclusively**. Previous versions supported multiple authentication methods, but this version focuses solely on GitHub OAuth for better security and user experience.
_NOTE_ : Authentication is made on behalf of a OAuth App which you must authorize for respective resources (e.g `org`)
1. **Automatic Setup**: The tool automatically retrieves the GitHub Client ID from the registry's health endpoint
2. **First Run Authentication**: When first run (or with the `--login` flag), the tool initiates the GitHub device flow
3. **User Authorization**: You'll be provided with a URL and a verification code to enter on GitHub
4. **Token Storage**: After successful authentication, the tool saves the access token locally in `.mcpregistry_token` for future use
5. **Secure Communication**: The token is sent in the HTTP Authorization header with the Bearer scheme for all registry API calls
**Note**: Authentication is performed via GitHub OAuth App, which you must authorize for the respective resources (e.g., organization access if publishing organization repositories).
## Example
@ -98,7 +99,9 @@ _NOTE_ : Authentication is made on behalf of a OAuth App which you must authoriz
## Important Notes
- The GitHub Client ID is automatically retrieved from the registry's health endpoint
- The authentication token is saved in a file named `.mcpregistry_token` in the current directory
- The tool requires an active internet connection to authenticate with GitHub and communicate with the registry
- Make sure the repository and package mentioned in your `mcp.json` file exist and are accessible
- **GitHub Authentication Only**: The tool exclusively uses GitHub OAuth device flow for authentication
- **Automatic Client ID**: The GitHub Client ID is automatically retrieved from the registry's health endpoint
- **Token Storage**: The authentication token is saved in `.mcpregistry_token` in the current directory
- **Internet Required**: Active internet connection needed for GitHub authentication and registry communication
- **Repository Access**: Ensure the repository and package mentioned in your `mcp.json` file exist and are accessible
- **OAuth Permissions**: You may need to grant the OAuth app access to your GitHub organizations if publishing org repositories

View file

@ -0,0 +1,104 @@
# Authentication System
The publisher tool now uses an interface-based authentication system that allows for multiple authentication mechanisms.
## Architecture
### Provider Interface
The `Provider` interface is defined in `auth/interface.go` and provides the following methods:
- `GetToken(ctx context.Context) (string, error)` - Retrieves or generates an authentication token
- `NeedsLogin() bool` - Checks if a new login flow is required
- `Login(ctx context.Context) error` - Performs the authentication flow
- `Name() string` - Returns the name of the authentication provider
### Available Authentication Providers
#### 1. GitHub OAuth Provider
- **Location**: `auth/github/oauth.go`
- **Usage**: Uses GitHub's device flow for authentication
- **Example**: `github.NewOAuthProvider(forceLogin, registryURL)`
## How to Add New Authentication Providers
1. Create a new package under `auth/` directory (e.g., `auth/custom/`)
2. Implement the `Provider` interface
3. Add any necessary configuration or initialization functions
4. Update the main application to use the new provider
### Example Implementation
```go
package custom
import (
"context"
"fmt"
)
type CustomProvider struct {
// your custom fields
}
func NewCustomProvider(config string) *CustomProvider {
return &CustomProvider{
// initialize your provider
}
}
func (cp *CustomProvider) GetToken(ctx context.Context) (string, error) {
// implement token retrieval logic
return "custom-token", nil
}
func (cp *CustomProvider) NeedsLogin() bool {
// implement login check logic
return false
}
func (cp *CustomProvider) Login(ctx context.Context) error {
// implement authentication flow
return nil
}
func (cp *CustomProvider) Name() string {
return "custom-auth"
}
```
## Usage in Main Application
The main application automatically selects the appropriate authentication provider:
1. Uses `GitHub OAuth Provider` by default
2. Future providers can be added by extending the provider selection logic
```go
// Create the appropriate auth provider based on configuration
var authProvider auth.Provider
switch authMethod {
case "github-oauth":
log.Println("Using GitHub OAuth for authentication")
authProvider = github.NewOAuthProvider(forceLogin, registryURL)
default:
log.Printf("Unsupported authentication method: %s\n", authMethod)
return
}
// Check if login is needed and perform authentication
ctx := context.Background()
if authProvider.NeedsLogin() {
err := authProvider.Login(ctx)
if err != nil {
log.Printf("Failed to authenticate with %s: %s\n", authProvider.Name(), err.Error())
return
}
}
// Get the token
token, err := authProvider.GetToken(ctx)
```
This design allows for easy extension and testing of different authentication mechanisms while maintaining a clean separation of concerns.

View file

@ -0,0 +1,294 @@
package github
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
)
const (
tokenFilePath = ".mcpregistry_token" // #nosec:G101
// GitHub OAuth URLs
GitHubDeviceCodeURL = "https://github.com/login/device/code" // #nosec:G101
GitHubAccessTokenURL = "https://github.com/login/oauth/access_token" // #nosec:G101
)
// DeviceCodeResponse represents the response from GitHub's device code endpoint
type DeviceCodeResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// AccessTokenResponse represents the response from GitHub's access token endpoint
type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
Error string `json:"error,omitempty"`
}
// OAuthProvider implements the AuthProvider interface using GitHub's device flow
type OAuthProvider struct {
clientID string
forceLogin bool
registryURL string
}
// ServerHealthResponse represents the response from the health endpoint
type ServerHealthResponse struct {
Status string `json:"status"`
GitHubClientID string `json:"github_client_id"`
}
// NewOAuthProvider creates a new GitHub OAuth provider
func NewOAuthProvider(forceLogin bool, registryURL string) *OAuthProvider {
return &OAuthProvider{
forceLogin: forceLogin,
registryURL: registryURL,
}
}
// GetToken retrieves the GitHub access token
func (g *OAuthProvider) GetToken(_ context.Context) (string, error) {
return readToken()
}
// NeedsLogin checks if a new login is required
func (g *OAuthProvider) NeedsLogin() bool {
if g.forceLogin {
return true
}
_, statErr := os.Stat(tokenFilePath)
return os.IsNotExist(statErr)
}
// Login performs the GitHub device flow authentication
func (g *OAuthProvider) Login(ctx context.Context) error {
// If clientID is not set, try to retrieve it from the server's health endpoint
if g.clientID == "" {
clientID, err := getClientID(ctx, g.registryURL)
if err != nil {
return fmt.Errorf("error getting GitHub Client ID: %w", err)
}
g.clientID = clientID
}
// Device flow login logic using GitHub's device flow
// First, request a device code
deviceCode, userCode, verificationURI, err := g.requestDeviceCode(ctx)
if err != nil {
return fmt.Errorf("error requesting device code: %w", err)
}
// Display instructions to the user
log.Println("\nTo authenticate, please:")
log.Println("1. Go to:", verificationURI)
log.Println("2. Enter code:", userCode)
log.Println("3. Authorize this application")
// Poll for the token
log.Println("Waiting for authorization...")
token, err := g.pollForToken(ctx, deviceCode)
if err != nil {
return fmt.Errorf("error polling for token: %w", err)
}
// Store the token locally
err = saveToken(token)
if err != nil {
return fmt.Errorf("error saving token: %w", err)
}
log.Println("Successfully authenticated!")
return nil
}
// Name returns the name of this auth provider
func (g *OAuthProvider) Name() string {
return "github-oauth"
}
// requestDeviceCode initiates the device authorization flow
func (g *OAuthProvider) requestDeviceCode(ctx context.Context) (string, string, string, error) {
if g.clientID == "" {
return "", "", "", fmt.Errorf("GitHub Client ID is required for device flow login")
}
payload := map[string]string{
"client_id": g.clientID,
"scope": "read:org read:user",
}
jsonData, err := json.Marshal(payload)
if err != nil {
return "", "", "", err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GitHubDeviceCodeURL, bytes.NewBuffer(jsonData))
if err != nil {
return "", "", "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", "", "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", "", err
}
if resp.StatusCode != http.StatusOK {
return "", "", "", fmt.Errorf("request device code failed: %s", body)
}
var deviceCodeResp DeviceCodeResponse
err = json.Unmarshal(body, &deviceCodeResp)
if err != nil {
return "", "", "", err
}
return deviceCodeResp.DeviceCode, deviceCodeResp.UserCode, deviceCodeResp.VerificationURI, nil
}
// pollForToken polls for access token after user completes authorization
func (g *OAuthProvider) pollForToken(ctx context.Context, deviceCode string) (string, error) {
if g.clientID == "" {
return "", fmt.Errorf("GitHub Client ID is required for device flow login")
}
payload := map[string]string{
"client_id": g.clientID,
"device_code": deviceCode,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
}
jsonData, err := json.Marshal(payload)
if err != nil {
return "", err
}
// Default polling interval and expiration time
interval := 5 // seconds
expiresIn := 900 // 15 minutes
deadline := time.Now().Add(time.Duration(expiresIn) * time.Second)
for time.Now().Before(deadline) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GitHubAccessTokenURL, bytes.NewBuffer(jsonData))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "", err
}
var tokenResp AccessTokenResponse
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return "", err
}
if tokenResp.Error == "authorization_pending" {
// User hasn't authorized yet, wait and retry
time.Sleep(time.Duration(interval) * time.Second)
continue
}
if tokenResp.Error != "" {
return "", fmt.Errorf("token request failed: %s", tokenResp.Error)
}
if tokenResp.AccessToken != "" {
return tokenResp.AccessToken, nil
}
// If we reach here, something unexpected happened
return "", fmt.Errorf("failed to obtain access token")
}
return "", fmt.Errorf("device code authorization timed out")
}
// saveToken saves the GitHub access token to a local file
func saveToken(token string) error {
return os.WriteFile(tokenFilePath, []byte(token), 0600)
}
// readToken reads the GitHub access token from a local file
func readToken() (string, error) {
tokenData, err := os.ReadFile(tokenFilePath)
if err != nil {
return "", err
}
return string(tokenData), nil
}
func getClientID(ctx context.Context, registryURL string) (string, error) {
// This function should retrieve the GitHub Client ID from the registry URL
// For now, we will return a placeholder value
// In a real implementation, this would likely involve querying the registry or configuration
if registryURL == "" {
return "", fmt.Errorf("registry URL is required to get GitHub Client ID")
}
// get the clientID from the server's health endpoint
healthURL := registryURL + "/v0/health"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
if err != nil {
log.Printf("Error creating request: %s\n", err.Error())
return "", err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Printf("Error fetching health endpoint: %s\n", err.Error())
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.Printf("Health endpoint returned status %d: %s\n", resp.StatusCode, body)
return "", fmt.Errorf("health endpoint returned status %d: %s", resp.StatusCode, body)
}
var healthResponse ServerHealthResponse
err = json.NewDecoder(resp.Body).Decode(&healthResponse)
if err != nil {
log.Printf("Error decoding health response: %s\n", err.Error())
return "", err
}
if healthResponse.GitHubClientID == "" {
log.Println("GitHub Client ID is not set in the server's health response.")
return "", fmt.Errorf("GitHub Client ID is not set in the server's health response")
}
githubClientID := healthResponse.GitHubClientID
return githubClientID, nil
}

View file

@ -0,0 +1,21 @@
package auth
import "context"
// Provider defines the interface for authentication mechanisms
type Provider interface {
// GetToken retrieves or generates an authentication token
// It returns the token string and any error encountered
GetToken(ctx context.Context) (string, error)
// NeedsLogin checks if a new login is required
// This can check for existing tokens, expiry, etc.
NeedsLogin() bool
// Login performs the authentication flow
// This might involve user interaction, device flows, etc.
Login(ctx context.Context) error
// Name returns the name of the authentication provider
Name() string
}

View file

@ -11,48 +11,22 @@ import (
"net/http"
"os"
"strings"
"time"
"github.com/modelcontextprotocol/registry/tools/publisher/auth"
"github.com/modelcontextprotocol/registry/tools/publisher/auth/github"
)
const (
tokenFilePath = ".mcpregistry_token" // #nosec:G101
// GitHub OAuth URLs
GitHubDeviceCodeURL = "https://github.com/login/device/code" // #nosec:G101
GitHubAccessTokenURL = "https://github.com/login/oauth/access_token" // #nosec:G101
)
// DeviceCodeResponse represents the response from GitHub's device code endpoint
type DeviceCodeResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// AccessTokenResponse represents the response from GitHub's access token endpoint
type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
Error string `json:"error,omitempty"`
}
type ServerHealthResponse struct {
Status string `json:"status"`
GitHubClientID string `json:"github_client_id"`
}
func main() {
var registryURL string
var mcpFilePath string
var forceLogin bool
var providedToken string
var authMethod string
flag.StringVar(&registryURL, "registry-url", "", "URL of the registry(required)")
flag.StringVar(&mcpFilePath, "mcp-file", "", "path to the MCP file(required)")
// Command-line flags for configuration
flag.StringVar(&registryURL, "registry-url", "", "URL of the registry (required)")
flag.StringVar(&mcpFilePath, "mcp-file", "", "path to the MCP file (required)")
flag.BoolVar(&forceLogin, "login", false, "force a new login even if a token exists")
flag.StringVar(&providedToken, "token", "", "use the provided token instead of GitHub authentication")
flag.StringVar(&authMethod, "auth-method", "github-oauth", "authentication method to use (default: github-oauth)")
flag.Parse()
@ -61,64 +35,6 @@ func main() {
return
}
// get the clientID from the server's health endpoint
healthURL := registryURL + "/v0/health"
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, healthURL, nil)
if err != nil {
log.Printf("Error creating request: %s\n", err.Error())
return
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Printf("Error fetching health endpoint: %s\n", err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.Printf("Health endpoint returned status %d: %s\n", resp.StatusCode, body)
return
}
var healthResponse ServerHealthResponse
err = json.NewDecoder(resp.Body).Decode(&healthResponse)
if err != nil {
log.Printf("Error decoding health response: %s\n", err.Error())
return
}
if healthResponse.GitHubClientID == "" {
log.Println("GitHub Client ID is not set in the server's health response.")
return
}
githubClientID := healthResponse.GitHubClientID
var token string
// If a token is provided via the command line, use it
if providedToken != "" {
token = providedToken
} else {
// Check if token exists or force login is requested
_, statErr := os.Stat(tokenFilePath)
if forceLogin || os.IsNotExist(statErr) {
err := performDeviceFlowLogin(githubClientID)
if err != nil {
log.Printf("Failed to perform device flow login: %s\n", err.Error())
return
}
}
// Read the token from the file
var err error
token, err = readToken()
if err != nil {
log.Printf("Error reading token: %s\n", err.Error())
return
}
}
// Read MCP file
mcpData, err := os.ReadFile(mcpFilePath)
if err != nil {
@ -126,6 +42,33 @@ func main() {
return
}
var authProvider auth.Provider // Determine the authentication method
switch authMethod {
case "github-oauth":
log.Println("Using GitHub OAuth for authentication")
authProvider = github.NewOAuthProvider(forceLogin, registryURL)
default:
log.Printf("Unsupported authentication method: %s\n", authMethod)
return
}
// Check if login is needed and perform authentication
ctx := context.Background()
if authProvider.NeedsLogin() {
err := authProvider.Login(ctx)
if err != nil {
log.Printf("Failed to authenticate with %s: %s\n", authProvider.Name(), err.Error())
return
}
}
// Get the token
token, err := authProvider.GetToken(ctx)
if err != nil {
log.Printf("Error getting token from %s: %s\n", authProvider.Name(), err.Error())
return
}
// Publish to registry
err = publishToRegistry(registryURL, mcpData, token)
if err != nil {
@ -136,172 +79,6 @@ func main() {
log.Println("Successfully published to registry!")
}
func performDeviceFlowLogin(githubClientID string) error {
if githubClientID == "" {
return fmt.Errorf("GitHub Client ID is required for device flow login")
}
// Device flow login logic using GitHub's device flow
// First, request a device code
deviceCode, userCode, verificationURI, err := requestDeviceCode(githubClientID)
if err != nil {
return fmt.Errorf("error requesting device code: %w", err)
}
// Display instructions to the user
log.Println("\nTo authenticate, please:")
log.Println("1. Go to:", verificationURI)
log.Println("2. Enter code:", userCode)
log.Println("3. Authorize this application")
// Poll for the token
log.Println("Waiting for authorization...")
token, err := pollForToken(deviceCode, githubClientID)
if err != nil {
return fmt.Errorf("error polling for token: %w", err)
}
// Store the token locally
err = saveToken(token)
if err != nil {
return fmt.Errorf("error saving token: %w", err)
}
log.Println("Successfully authenticated!")
return nil
}
// requestDeviceCode initiates the device authorization flow
func requestDeviceCode(githubClientID string) (string, string, string, error) {
if githubClientID == "" {
return "", "", "", fmt.Errorf("GitHub Client ID is required for device flow login")
}
payload := map[string]string{
"client_id": githubClientID,
"scope": "read:org read:user",
}
jsonData, err := json.Marshal(payload)
if err != nil {
return "", "", "", err
}
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, GitHubDeviceCodeURL, bytes.NewBuffer(jsonData))
if err != nil {
return "", "", "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", "", "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", "", err
}
if resp.StatusCode != http.StatusOK {
return "", "", "", fmt.Errorf("request device code failed: %s", body)
}
var deviceCodeResp DeviceCodeResponse
err = json.Unmarshal(body, &deviceCodeResp)
if err != nil {
return "", "", "", err
}
return deviceCodeResp.DeviceCode, deviceCodeResp.UserCode, deviceCodeResp.VerificationURI, nil
}
// pollForToken polls for access token after user completes authorization
func pollForToken(deviceCode, githubClientID string) (string, error) {
if githubClientID == "" {
return "", fmt.Errorf("GitHub Client ID is required for device flow login")
}
payload := map[string]string{
"client_id": githubClientID,
"device_code": deviceCode,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
}
jsonData, err := json.Marshal(payload)
if err != nil {
return "", err
}
// Default polling interval and expiration time
interval := 5 // seconds
expiresIn := 900 // 15 minutes
deadline := time.Now().Add(time.Duration(expiresIn) * time.Second)
for time.Now().Before(deadline) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, GitHubAccessTokenURL, bytes.NewBuffer(jsonData))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "", err
}
var tokenResp AccessTokenResponse
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return "", err
}
if tokenResp.Error == "authorization_pending" {
// User hasn't authorized yet, wait and retry
time.Sleep(time.Duration(interval) * time.Second)
continue
}
if tokenResp.Error != "" {
return "", fmt.Errorf("token request failed: %s", tokenResp.Error)
}
if tokenResp.AccessToken != "" {
return tokenResp.AccessToken, nil
}
// If we reach here, something unexpected happened
return "", fmt.Errorf("failed to obtain access token")
}
return "", fmt.Errorf("device code authorization timed out")
}
// saveToken saves the GitHub access token to a local file
func saveToken(token string) error {
return os.WriteFile(tokenFilePath, []byte(token), 0600)
}
// readToken reads the GitHub access token from a local file
func readToken() (string, error) {
tokenData, err := os.ReadFile(tokenFilePath)
if err != nil {
return "", err
}
return string(tokenData), nil
}
// publishToRegistry sends the MCP server details to the registry with authentication
func publishToRegistry(registryURL string, mcpData []byte, token string) error {
// Parse the MCP JSON data