From 835499d8d78d61344abb5ac9f412690b9d6ee64c Mon Sep 17 00:00:00 2001 From: Jason Bao Date: Wed, 1 Apr 2026 11:07:04 -0700 Subject: [PATCH] Ensure auth command for mcp --- cmd/user/ensure_auth.go | 123 +++++++++++++++++++++++++++ cmd/user/user.go | 1 + plugins/major/.mcp.json | 4 +- plugins/major/scripts/get-headers.sh | 5 +- 4 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 cmd/user/ensure_auth.go diff --git a/cmd/user/ensure_auth.go b/cmd/user/ensure_auth.go new file mode 100644 index 0000000..7886a88 --- /dev/null +++ b/cmd/user/ensure_auth.go @@ -0,0 +1,123 @@ +package user + +import ( + "fmt" + "os" + + mjrToken "github.com/major-technology/cli/clients/token" + clierrors "github.com/major-technology/cli/errors" + "github.com/major-technology/cli/singletons" + "github.com/major-technology/cli/utils" + "github.com/spf13/cobra" +) + +var ensureAuthCmd = &cobra.Command{ + Use: "ensure-auth", + Short: "Ensure valid authentication, running login if token is missing or expired", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runEnsureAuth(cmd) + }, +} + +func runEnsureAuth(cmd *cobra.Command) error { + // All UI output goes to stderr so stdout contains only the token. + cmd.SetOut(os.Stderr) + cmd.SetErr(os.Stderr) + + token, needsLogin := checkExistingAuth() + if !needsLogin { + fmt.Fprint(os.Stdout, token) + return nil + } + + if err := runBrowserLogin(cmd); err != nil { + return err + } + + if err := ensureOrgSelected(cmd); err != nil { + return err + } + + newToken, err := mjrToken.GetToken() + if err != nil { + return clierrors.WrapError("failed to get token after login", err) + } + fmt.Fprint(os.Stdout, newToken) + return nil +} + +// checkExistingAuth returns (token, needsLogin). If the stored token is valid, +// it returns the token with needsLogin=false. Otherwise needsLogin=true. +func checkExistingAuth() (string, bool) { + token, err := mjrToken.GetToken() + if err != nil || token == "" { + return "", true + } + + client := singletons.GetAPIClient() + _, err = client.VerifyToken() + if err != nil { + return "", true + } + + return token, false +} + +// runBrowserLogin performs the device-code login flow (opens browser, polls). +func runBrowserLogin(cmd *cobra.Command) error { + client := singletons.GetAPIClient() + startResp, err := client.StartLogin() + if err != nil { + return clierrors.WrapError("failed to start login", err) + } + + _ = utils.OpenBrowser(startResp.VerificationURI) + cmd.Println("Session expired. Opening browser for authentication...") + cmd.Printf("If the browser doesn't open, visit:\n%s\n", startResp.VerificationURI) + + token, err := pollForToken(cmd, client, startResp.DeviceCode, startResp.Interval, startResp.ExpiresIn) + if err != nil { + return clierrors.WrapError("authentication failed", err) + } + + if err := mjrToken.StoreToken(token); err != nil { + return clierrors.WrapError("failed to store token", err) + } + + return nil +} + +// ensureOrgSelected verifies an org is set. If not, it auto-selects when +// there's exactly one org, or returns an error asking the user to pick one. +func ensureOrgSelected(cmd *cobra.Command) error { + orgID, _, err := mjrToken.GetDefaultOrg() + if err == nil && orgID != "" { + return nil + } + + client := singletons.GetAPIClient() + orgsResp, err := client.GetOrganizations() + if err != nil { + return clierrors.WrapError("failed to fetch organizations", err) + } + + if len(orgsResp.Organizations) == 0 { + return clierrors.ErrorNoOrganizationsAvailable + } + + if len(orgsResp.Organizations) == 1 { + org := orgsResp.Organizations[0] + if err := mjrToken.StoreDefaultOrg(org.ID, org.Name); err != nil { + return clierrors.WrapError("failed to store default organization", err) + } + cmd.Printf("Organization set to: %s\n", org.Name) + return nil + } + + return &clierrors.CLIError{ + Title: "Multiple organizations available", + Suggestion: "Run 'major org select' to choose a default organization.", + Err: fmt.Errorf("interactive org selection required"), + } +} diff --git a/cmd/user/user.go b/cmd/user/user.go index 3fdc12b..82723a5 100644 --- a/cmd/user/user.go +++ b/cmd/user/user.go @@ -26,4 +26,5 @@ func init() { Cmd.AddCommand(whoamiCmd) Cmd.AddCommand(gitconfigCmd) Cmd.AddCommand(tokenCmd) + Cmd.AddCommand(ensureAuthCmd) } diff --git a/plugins/major/.mcp.json b/plugins/major/.mcp.json index ac52af7..86c1b1b 100644 --- a/plugins/major/.mcp.json +++ b/plugins/major/.mcp.json @@ -2,11 +2,11 @@ "major-resources": { "type": "http", "url": "https://go-api.prod.major.build/cli/v1/mcp", - "headersHelper": "bash -c 'TOKEN=$(major user token 2>/dev/null); ORG=$(major org id 2>/dev/null); echo \"{\\\"Authorization\\\": \\\"Bearer $TOKEN\\\", \\\"x-major-org-id\\\": \\\"$ORG\\\"}\"'" + "headersHelper": "bash -c 'TOKEN=$(major user ensure-auth); [ -z \"$TOKEN\" ] && exit 1; ORG=$(major org id 2>/dev/null); [ -z \"$ORG\" ] && exit 1; echo \"{\\\"Authorization\\\": \\\"Bearer $TOKEN\\\", \\\"x-major-org-id\\\": \\\"$ORG\\\"}\"'" }, "major-platform": { "type": "http", "url": "https://api.prod.major.build/mcp/cli", - "headersHelper": "bash -c 'TOKEN=$(major user token 2>/dev/null); ORG=$(major org id 2>/dev/null); echo \"{\\\"Authorization\\\": \\\"Bearer $TOKEN\\\", \\\"x-major-org-id\\\": \\\"$ORG\\\"}\"'" + "headersHelper": "bash -c 'TOKEN=$(major user ensure-auth); [ -z \"$TOKEN\" ] && exit 1; ORG=$(major org id 2>/dev/null); [ -z \"$ORG\" ] && exit 1; echo \"{\\\"Authorization\\\": \\\"Bearer $TOKEN\\\", \\\"x-major-org-id\\\": \\\"$ORG\\\"}\"'" } } diff --git a/plugins/major/scripts/get-headers.sh b/plugins/major/scripts/get-headers.sh index 26ecc40..a662811 100755 --- a/plugins/major/scripts/get-headers.sh +++ b/plugins/major/scripts/get-headers.sh @@ -7,7 +7,8 @@ done [ -z "$MAJOR" ] && MAJOR=$(PATH="$HOME/.major/bin:$HOME/go/bin:/usr/local/bin:$PATH" command -v major 2>/dev/null) [ -z "$MAJOR" ] && exit 1 -TOKEN=$("$MAJOR" user token 2>/dev/null) +TOKEN=$("$MAJOR" user ensure-auth) +[ -z "$TOKEN" ] && exit 1 ORG=$("$MAJOR" org id 2>/dev/null) -[ -z "$TOKEN" ] || [ -z "$ORG" ] && exit 1 +[ -z "$ORG" ] && exit 1 echo "{\"Authorization\": \"Bearer $TOKEN\", \"x-major-org-id\": \"$ORG\"}"