From 8ebd9dc76d900ed5c4e5da8f21d494e0b1eb942d Mon Sep 17 00:00:00 2001 From: Rishikesh Date: Thu, 9 Apr 2026 10:23:01 -0700 Subject: [PATCH 1/2] feat: add 'major resource connect' command Opens the /connect page in the browser for per-user OAuth resources. Takes resource IDs as positional args and an optional --environment flag. If --environment is not provided, resolves from the app's current environment. Also adds AuthMode field to ResourceItem struct for discoverability. Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/api/structs.go | 1 + cmd/resource/connect.go | 85 +++++++++++++++++++ cmd/resource/resource.go | 1 + .../skills/resources_googlecalendar/SKILL.md | 11 +++ 4 files changed, 98 insertions(+) create mode 100644 cmd/resource/connect.go diff --git a/clients/api/structs.go b/clients/api/structs.go index 2298ebe..ec460be 100644 --- a/clients/api/structs.go +++ b/clients/api/structs.go @@ -100,6 +100,7 @@ type ResourceItem struct { Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` + AuthMode string `json:"authMode"` } // GetApplicationResourcesResponse represents the response from GET /applications/:applicationId/resources diff --git a/cmd/resource/connect.go b/cmd/resource/connect.go new file mode 100644 index 0000000..d90ac15 --- /dev/null +++ b/cmd/resource/connect.go @@ -0,0 +1,85 @@ +package resource + +import ( + "fmt" + "strings" + + "github.com/major-technology/cli/errors" + "github.com/major-technology/cli/middleware" + "github.com/major-technology/cli/singletons" + "github.com/major-technology/cli/utils" + "github.com/spf13/cobra" +) + +var environmentFlag string + +var connectCmd = &cobra.Command{ + Use: "connect [resourceId...]", + Short: "Open the browser to connect your OAuth accounts for per-user resources", + Long: `Opens the /connect page in your browser so you can authenticate with +per-user OAuth resources. Resource IDs can be found via the web UI connectors page +or from the list_resources MCP tool. + +If --environment is not provided, the app's current environment is used +(resolved from the git remote in the current directory).`, + Args: cobra.MinimumNArgs(1), + PreRunE: middleware.Compose( + middleware.CheckLogin, + ), + RunE: func(cmd *cobra.Command, args []string) error { + return runConnect(cmd, args) + }, +} + +func init() { + connectCmd.Flags().StringVar(&environmentFlag, "environment", "", "Environment ID (defaults to app's current environment)") +} + +func runConnect(cmd *cobra.Command, resourceIds []string) error { + cfg := singletons.GetConfig() + if cfg == nil { + return fmt.Errorf("configuration not initialized") + } + + envID := environmentFlag + + if envID == "" { + // Resolve environment from app context + appInfo, err := utils.GetApplicationInfo("") + if err != nil { + return &errors.CLIError{ + Title: "Could not resolve environment", + Suggestion: "Use --environment or run this command from an app directory with a git remote.", + Err: err, + } + } + + apiClient := singletons.GetAPIClient() + + envResp, err := apiClient.GetApplicationEnvironment(appInfo.ApplicationID) + if err != nil { + return errors.WrapError("failed to get application environment", err) + } + + if envResp.EnvironmentID == nil { + return &errors.CLIError{ + Title: "No environment set", + Suggestion: "Use --environment or run 'major resource env' to choose one.", + } + } + + envID = *envResp.EnvironmentID + } + + // Build the connect URL + resources := strings.Join(resourceIds, ",") + connectURL := fmt.Sprintf("%s/connect?resources=%s&environmentId=%s", cfg.FrontendURI, resources, envID) + + if err := utils.OpenBrowser(connectURL); err != nil { + cmd.Printf("Failed to open browser automatically. Please visit:\n%s\n", connectURL) + return nil + } + + cmd.Printf("Opening connect page in your browser:\n%s\n", connectURL) + return nil +} diff --git a/cmd/resource/resource.go b/cmd/resource/resource.go index 1f6fe3b..90880ec 100644 --- a/cmd/resource/resource.go +++ b/cmd/resource/resource.go @@ -25,4 +25,5 @@ func init() { Cmd.AddCommand(listCmd) Cmd.AddCommand(addCmd) Cmd.AddCommand(removeCmd) + Cmd.AddCommand(connectCmd) } diff --git a/plugins/shared/skills/resources_googlecalendar/SKILL.md b/plugins/shared/skills/resources_googlecalendar/SKILL.md index 476d326..b6ebf5a 100644 --- a/plugins/shared/skills/resources_googlecalendar/SKILL.md +++ b/plugins/shared/skills/resources_googlecalendar/SKILL.md @@ -86,3 +86,14 @@ const createResult = await googleCalendarClient.invoke("POST", "calendars/primar - **Scope presets**: The resource may be configured as "readonly" (can only read) or "readwrite" (can read and create/modify events). Write operations will fail with 403 if the resource is readonly. **Docs**: [Google Calendar API Reference](https://developers.google.com/calendar/api/v3/reference) + +--- + +## Per-User OAuth + +Google Calendar uses per-user OAuth — each user must connect their own Google account (`requiresUserOAuth: true` in `list_resources`). Before using Google Calendar tools: + +1. Call `setup-user-oauth` with the resource ID +2. If credentials are missing, present the user with the connect URL +3. **CLI users:** Tell them to run `major resource connect --environment ` +4. Wait for the user to confirm they've connected, then retry the resource tools From da8a82b4c675144b69139cf85cb64009e6cdfb52 Mon Sep 17 00:00:00 2001 From: Rishikesh Date: Fri, 10 Apr 2026 13:19:43 -0700 Subject: [PATCH 2/2] feat: improve per-user OAuth handling in skills - Rewrite Google Calendar skill to lead with per-user OAuth setup - Add per-user OAuth step to list-resources skill - Skills detect context (web vs CLI) by checking tool availability - CLI agent runs `major resource connect` itself via Bash Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/resource/connect.go | 43 +++++++------------ .../skills/resources_googlecalendar/SKILL.md | 22 +++------- .../skills/resources_list-resources/SKILL.md | 11 +++++ 3 files changed, 34 insertions(+), 42 deletions(-) diff --git a/cmd/resource/connect.go b/cmd/resource/connect.go index d90ac15..615e089 100644 --- a/cmd/resource/connect.go +++ b/cmd/resource/connect.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/major-technology/cli/errors" "github.com/major-technology/cli/middleware" "github.com/major-technology/cli/singletons" "github.com/major-technology/cli/utils" @@ -20,8 +19,9 @@ var connectCmd = &cobra.Command{ per-user OAuth resources. Resource IDs can be found via the web UI connectors page or from the list_resources MCP tool. -If --environment is not provided, the app's current environment is used -(resolved from the git remote in the current directory).`, +If --environment is not provided, the environment is auto-resolved from +the app's git remote. If that also fails, the connect page will use +the org's default environment.`, Args: cobra.MinimumNArgs(1), PreRunE: middleware.Compose( middleware.CheckLogin, @@ -41,39 +41,28 @@ func runConnect(cmd *cobra.Command, resourceIds []string) error { return fmt.Errorf("configuration not initialized") } + // Build the connect URL + resources := strings.Join(resourceIds, ",") + connectURL := fmt.Sprintf("%s/connect?resources=%s", cfg.FrontendURI, resources) + + // Append environment if explicitly provided or resolvable from app context envID := environmentFlag if envID == "" { - // Resolve environment from app context appInfo, err := utils.GetApplicationInfo("") - if err != nil { - return &errors.CLIError{ - Title: "Could not resolve environment", - Suggestion: "Use --environment or run this command from an app directory with a git remote.", - Err: err, - } - } - - apiClient := singletons.GetAPIClient() + if err == nil { + apiClient := singletons.GetAPIClient() - envResp, err := apiClient.GetApplicationEnvironment(appInfo.ApplicationID) - if err != nil { - return errors.WrapError("failed to get application environment", err) - } - - if envResp.EnvironmentID == nil { - return &errors.CLIError{ - Title: "No environment set", - Suggestion: "Use --environment or run 'major resource env' to choose one.", + envResp, err := apiClient.GetApplicationEnvironment(appInfo.ApplicationID) + if err == nil && envResp.EnvironmentID != nil { + envID = *envResp.EnvironmentID } } - - envID = *envResp.EnvironmentID } - // Build the connect URL - resources := strings.Join(resourceIds, ",") - connectURL := fmt.Sprintf("%s/connect?resources=%s&environmentId=%s", cfg.FrontendURI, resources, envID) + if envID != "" { + connectURL += fmt.Sprintf("&environmentId=%s", envID) + } if err := utils.OpenBrowser(connectURL); err != nil { cmd.Printf("Failed to open browser automatically. Please visit:\n%s\n", connectURL) diff --git a/plugins/shared/skills/resources_googlecalendar/SKILL.md b/plugins/shared/skills/resources_googlecalendar/SKILL.md index b6ebf5a..8ed3692 100644 --- a/plugins/shared/skills/resources_googlecalendar/SKILL.md +++ b/plugins/shared/skills/resources_googlecalendar/SKILL.md @@ -5,14 +5,16 @@ description: Implements Google Calendar event management and scheduling using ge # Major Platform Resource: Google Calendar -## Setting Up a Google Calendar Connector +## Per-User OAuth (required before use) -Google Calendar requires OAuth authentication before use. +Google Calendar uses per-user OAuth — each user must connect their own Google account before any tools will work (`requiresUserOAuth: true` in `list_resources`). -### When the user asks you to set up Google Calendar or connect their calendar: +**Before calling any Google Calendar tools**, check if the `mcp__user-oauth-setup__setup-user-oauth` tool is available: -1. Call `mcp__resource-setup__request-resource-setup` with `subtype: "googlecalendar"` — this prompts the user to authenticate with Google -2. Once setup completes, the resource is ready to use +- **If available** (web ai-coder): Call `setup-user-oauth` with the resource ID. It will prompt the user to connect via a popup. +- **If not available** (CLI): Run `major resource connect ` via the Bash tool — this opens the user's browser to complete OAuth. Then ask the user to confirm they've connected before retrying the resource tools. + +Do NOT attempt to call Google Calendar tools without completing this step — the calls will fail. --- @@ -87,13 +89,3 @@ const createResult = await googleCalendarClient.invoke("POST", "calendars/primar **Docs**: [Google Calendar API Reference](https://developers.google.com/calendar/api/v3/reference) ---- - -## Per-User OAuth - -Google Calendar uses per-user OAuth — each user must connect their own Google account (`requiresUserOAuth: true` in `list_resources`). Before using Google Calendar tools: - -1. Call `setup-user-oauth` with the resource ID -2. If credentials are missing, present the user with the connect URL -3. **CLI users:** Tell them to run `major resource connect --environment ` -4. Wait for the user to confirm they've connected, then retry the resource tools diff --git a/plugins/shared/skills/resources_list-resources/SKILL.md b/plugins/shared/skills/resources_list-resources/SKILL.md index 285be62..bb9454d 100644 --- a/plugins/shared/skills/resources_list-resources/SKILL.md +++ b/plugins/shared/skills/resources_list-resources/SKILL.md @@ -29,3 +29,14 @@ For each resource you plan to use: The agent will download the file, read it, and return a summary plus the local file path. 4. If the context document contains schema or API information, use it directly — do not make redundant queries (e.g. do not run `\d` table commands if the schema is already in the context doc) 5. Tell the user which context documents you read and what you learned, so they know their context is being used + +## Step 3: Handle per-user OAuth resources + +Some resources require each user to connect their own account (`requiresUserOAuth: true` in the `list_resources` response). **You must handle this before attempting to use those resources.** + +Check if the `mcp__user-oauth-setup__setup-user-oauth` tool is available: + +- **If available** (web ai-coder): Call `setup-user-oauth` with the resource ID. It will prompt the user to connect via a popup and block until complete. +- **If not available** (CLI): Run `major resource connect ` via the Bash tool — this opens the user's browser to complete OAuth. Then ask the user to confirm they've connected before retrying the resource tools. + +Do NOT attempt to call resource tools for a `requiresUserOAuth: true` resource without completing this step first — the calls will fail with an authentication error.