diff --git a/cmd/publisher/auth/github-at.go b/cmd/publisher/auth/github-at.go index 59bfd0b..602e116 100644 --- a/cmd/publisher/auth/github-at.go +++ b/cmd/publisher/auth/github-at.go @@ -50,9 +50,10 @@ type StoredRegistryToken struct { // GitHubATProvider implements the Provider interface using GitHub's device flow type GitHubATProvider struct { - clientID string - forceLogin bool - registryURL string + clientID string + forceLogin bool + registryURL string + providedToken string // Token provided via --token flag or MCP_GITHUB_TOKEN env var } // ServerHealthResponse represents the response from the health endpoint @@ -62,10 +63,16 @@ type ServerHealthResponse struct { } // NewGitHubATProvider creates a new GitHub OAuth provider -func NewGitHubATProvider(forceLogin bool, registryURL string) Provider { +func NewGitHubATProvider(forceLogin bool, registryURL, token string) Provider { + // Check for token from flag or environment variable + if token == "" { + token = os.Getenv("MCP_GITHUB_TOKEN") + } + return &GitHubATProvider{ - forceLogin: forceLogin, - registryURL: registryURL, + forceLogin: forceLogin, + registryURL: registryURL, + providedToken: token, } } @@ -100,6 +107,11 @@ func (g *GitHubATProvider) GetToken(ctx context.Context) (string, error) { // NeedsLogin checks if a new login is required func (g *GitHubATProvider) NeedsLogin() bool { + // If a token was provided via --token or MCP_GITHUB_TOKEN, no login needed + if g.providedToken != "" { + return false + } + if g.forceLogin { return true } @@ -123,6 +135,15 @@ func (g *GitHubATProvider) NeedsLogin() bool { // Login performs the GitHub device flow authentication func (g *GitHubATProvider) Login(ctx context.Context) error { + // If a token was provided via --token or MCP_GITHUB_TOKEN, save it and skip device flow + if g.providedToken != "" { + err := saveToken(g.providedToken) + if err != nil { + return fmt.Errorf("error saving provided token: %w", err) + } + return nil + } + // If clientID is not set, try to retrieve it from the server's health endpoint if g.clientID == "" { clientID, err := getClientID(ctx, g.registryURL) diff --git a/cmd/publisher/commands/init.go b/cmd/publisher/commands/init.go index dc4ec87..6a7381d 100644 --- a/cmd/publisher/commands/init.go +++ b/cmd/publisher/commands/init.go @@ -29,7 +29,7 @@ func InitCommand() error { description := detectDescription() version := "1.0.0" repoURL := detectRepoURL() - repoSource := "github" + repoSource := MethodGitHub if repoURL != "" && !strings.Contains(repoURL, "github.com") { if strings.Contains(repoURL, "gitlab.com") { repoSource = "gitlab" diff --git a/cmd/publisher/commands/login.go b/cmd/publisher/commands/login.go index 0b36bc0..091a700 100644 --- a/cmd/publisher/commands/login.go +++ b/cmd/publisher/commands/login.go @@ -18,6 +18,11 @@ import ( const ( DefaultRegistryURL = "https://registry.modelcontextprotocol.io" TokenFileName = ".mcp_publisher_token" //nolint:gosec // Not a credential, just a filename + MethodGitHub = "github" + MethodGitHubOIDC = "github-oidc" + MethodDNS = "dns" + MethodHTTP = "http" + MethodNone = "none" ) type CryptoAlgorithm auth.CryptoAlgorithm @@ -31,6 +36,7 @@ type LoginFlags struct { KvVault string KvKeyName string KmsResource string + Token Token CryptoAlgorithm CryptoAlgorithm SignerType SignerType ArgOffset int @@ -56,6 +62,8 @@ func (c *CryptoAlgorithm) Set(v string) error { return fmt.Errorf("invalid algorithm: %q (allowed: ed25519, ecdsap384)", v) } +type Token string + func parseLoginFlags(method string, args []string) (LoginFlags, error) { var flags LoginFlags loginFlags := flag.NewFlagSet("login", flag.ExitOnError) @@ -64,6 +72,12 @@ func parseLoginFlags(method string, args []string) (LoginFlags, error) { flags.ArgOffset = 1 loginFlags.StringVar(&flags.RegistryURL, "registry", DefaultRegistryURL, "Registry URL") + // Add --token flag for GitHub authentication + var token string + if method == MethodGitHub { + loginFlags.StringVar(&token, "token", "", "GitHub Personal Access Token") + } + if method == "dns" || method == "http" { loginFlags.StringVar(&flags.Domain, "domain", "", "Domain name") if len(args) > 1 { @@ -90,6 +104,11 @@ func parseLoginFlags(method string, args []string) (LoginFlags, error) { flags.RegistryURL = strings.TrimRight(flags.RegistryURL, "/") } + // Store the token in flags if it was provided + if method == MethodGitHub { + flags.Token = Token(token) + } + return flags, err } @@ -108,23 +127,23 @@ func createSigner(flags LoginFlags) (auth.Signer, error) { } } -func createAuthProvider(method, registryURL, domain string, signer auth.Signer) (auth.Provider, error) { +func createAuthProvider(method, registryURL, domain string, token Token, signer auth.Signer) (auth.Provider, error) { switch method { - case "github": - return auth.NewGitHubATProvider(true, registryURL), nil - case "github-oidc": + case MethodGitHub: + return auth.NewGitHubATProvider(true, registryURL, string(token)), nil + case MethodGitHubOIDC: return auth.NewGitHubOIDCProvider(registryURL), nil - case "dns": + case MethodDNS: if domain == "" { return nil, errors.New("dns authentication requires --domain") } return auth.NewDNSProvider(registryURL, domain, &signer), nil - case "http": + case MethodHTTP: if domain == "" { return nil, errors.New("http authentication requires --domain") } return auth.NewHTTPProvider(registryURL, domain, &signer), nil - case "none": + case MethodNone: return auth.NewNoneProvider(registryURL), nil default: return nil, fmt.Errorf("unknown authentication method: %s\nFor a list of available methods, run: mcp-publisher login", method) @@ -191,11 +210,10 @@ Examples: } } - authProvider, err := createAuthProvider(method, flags.RegistryURL, flags.Domain, signer) + authProvider, err := createAuthProvider(method, flags.RegistryURL, flags.Domain, flags.Token, signer) if err != nil { return err } - ctx := context.Background() _, _ = fmt.Fprintf(os.Stdout, "Logging in with %s...\n", method) diff --git a/docs/guides/publishing/github-actions.md b/docs/guides/publishing/github-actions.md index 13a54a8..61889a9 100644 --- a/docs/guides/publishing/github-actions.md +++ b/docs/guides/publishing/github-actions.md @@ -98,14 +98,25 @@ The workflow runs tests, builds your package, publishes to npm, and publishes to ### GitHub Personal Access Token +You can authenticate using a GitHub Personal Access Token in two ways: + +**Option 1: Using the --token flag** + ```yaml - name: Login to MCP Registry - run: mcp-publisher login github --token ${{ secrets.GITHUB_TOKEN }} - env: - GITHUB_TOKEN: ${{ secrets.MCP_GITHUB_TOKEN }} + run: mcp-publisher login github --token ${{ secrets.MCP_GITHUB_TOKEN }} ``` -Add `MCP_GITHUB_TOKEN` secret with a GitHub PAT that has repo access. +**Option 2: Using environment variable** + +```yaml +- name: Login to MCP Registry + run: mcp-publisher login github + env: + MCP_GITHUB_TOKEN: ${{ secrets.MCP_GITHUB_TOKEN }} +``` + +**Note:** You must create a custom secret `MCP_GITHUB_TOKEN` with a GitHub Personal Access Token that has `read:org` and `read:user` scopes. The automatic `GITHUB_TOKEN` provided by GitHub Actions does not have these permissions and cannot be used. ### DNS Authentication