diff --git a/tools/publisher/README.md b/tools/publisher/README.md index 4f0e2366..8efba59f 100644 --- a/tools/publisher/README.md +++ b/tools/publisher/README.md @@ -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 --mcp-file +./bin/mcp-publisher -registry-url -mcp-file # Force a new login even if a token exists -./bin/mcp-publisher --registry-url --mcp-file --login +./bin/mcp-publisher -registry-url -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 diff --git a/tools/publisher/auth/README.md b/tools/publisher/auth/README.md new file mode 100644 index 00000000..4f0e3af7 --- /dev/null +++ b/tools/publisher/auth/README.md @@ -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. diff --git a/tools/publisher/auth/github/oauth.go b/tools/publisher/auth/github/oauth.go new file mode 100644 index 00000000..d1e5afc0 --- /dev/null +++ b/tools/publisher/auth/github/oauth.go @@ -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 +} diff --git a/tools/publisher/auth/interface.go b/tools/publisher/auth/interface.go new file mode 100644 index 00000000..a2304298 --- /dev/null +++ b/tools/publisher/auth/interface.go @@ -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 +} diff --git a/tools/publisher/main.go b/tools/publisher/main.go index dcde2296..9b0c97ff 100644 --- a/tools/publisher/main.go +++ b/tools/publisher/main.go @@ -11,48 +11,22 @@ import ( "net/http" "os" "strings" - "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 + "github.com/modelcontextprotocol/registry/tools/publisher/auth" + "github.com/modelcontextprotocol/registry/tools/publisher/auth/github" ) -// 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(®istryURL, "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(®istryURL, "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,68 +35,37 @@ 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) + // Read MCP file + mcpData, err := os.ReadFile(mcpFilePath) if err != nil { - log.Printf("Error creating request: %s\n", err.Error()) + log.Printf("Error reading MCP file: %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.") + 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 } - 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() + // Check if login is needed and perform authentication + ctx := context.Background() + if authProvider.NeedsLogin() { + err := authProvider.Login(ctx) if err != nil { - log.Printf("Error reading token: %s\n", err.Error()) + log.Printf("Failed to authenticate with %s: %s\n", authProvider.Name(), err.Error()) return } } - // Read MCP file - mcpData, err := os.ReadFile(mcpFilePath) + // Get the token + token, err := authProvider.GetToken(ctx) if err != nil { - log.Printf("Error reading MCP file: %s\n", err.Error()) + log.Printf("Error getting token from %s: %s\n", authProvider.Name(), err.Error()) return } @@ -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