diff --git a/cmd/github-mcp-server/compare_scopes.go b/cmd/github-mcp-server/compare_scopes.go new file mode 100644 index 000000000..4479bf1c9 --- /dev/null +++ b/cmd/github-mcp-server/compare_scopes.go @@ -0,0 +1,311 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "sort" + "strings" + + "github.com/github/github-mcp-server/pkg/github" + "github.com/github/github-mcp-server/pkg/scopes" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// ScopeComparison represents the result of comparing token scopes with required scopes. +type ScopeComparison struct { + TokenScopes []string `json:"token_scopes"` + RequiredScopes []string `json:"required_scopes"` + MissingScopes []string `json:"missing_scopes"` + ExtraScopes []string `json:"extra_scopes"` + HasAllRequired bool `json:"has_all_required"` +} + +// CompareOutput is the full output structure for the compare-scopes command. +type CompareOutput struct { + Comparison ScopeComparison `json:"comparison"` + EnabledToolsets []string `json:"enabled_toolsets"` + ReadOnly bool `json:"read_only"` + Tools []ToolScopeInfo `json:"tools,omitempty"` + ScopesByTool map[string][]string `json:"scopes_by_tool,omitempty"` +} + +var compareScopesCmd = &cobra.Command{ + Use: "compare-scopes", + Short: "Compare PAT token scopes with required scopes", + Long: `Compare the OAuth scopes granted to a PAT token with the scopes required by enabled tools. + +This command fetches the scopes from your GitHub Personal Access Token and compares +them with the scopes required by the enabled tools. It reports any missing or extra +scopes to help you understand if your token has the necessary permissions. + +The token is read from the GITHUB_PERSONAL_ACCESS_TOKEN environment variable or +can be provided via the --token flag. + +The output format can be controlled with the --output flag: + - text (default): Human-readable text output with colored indicators + - json: JSON output for programmatic use + +Examples: + # Compare using token from environment variable + export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_... + github-mcp-server compare-scopes + + # Compare for specific toolsets + github-mcp-server compare-scopes --toolsets=repos,issues,pull_requests + + # Compare with token from flag + github-mcp-server compare-scopes --token=ghp_... + + # Output as JSON + github-mcp-server compare-scopes --output=json`, + RunE: func(_ *cobra.Command, _ []string) error { + return runCompareScopes() + }, +} + +func init() { + compareScopesCmd.Flags().StringP("output", "o", "text", "Output format: text or json") + compareScopesCmd.Flags().String("token", "", "GitHub Personal Access Token (overrides GITHUB_PERSONAL_ACCESS_TOKEN env var)") + _ = viper.BindPFlag("compare-scopes-output", compareScopesCmd.Flags().Lookup("output")) + _ = viper.BindPFlag("compare-scopes-token", compareScopesCmd.Flags().Lookup("token")) + + rootCmd.AddCommand(compareScopesCmd) +} + +func runCompareScopes() error { + // Get token from flag or environment variable + token := viper.GetString("compare-scopes-token") + if token == "" { + token = viper.GetString("personal_access_token") + } + if token == "" { + return fmt.Errorf("GitHub Personal Access Token not provided. Set GITHUB_PERSONAL_ACCESS_TOKEN or use --token flag") + } + + // Get toolsets configuration (same logic as list-scopes) + var enabledToolsets []string + if viper.IsSet("toolsets") { + if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil { + return fmt.Errorf("failed to unmarshal toolsets: %w", err) + } + } + + // Get specific tools + var enabledTools []string + if viper.IsSet("tools") { + if err := viper.UnmarshalKey("tools", &enabledTools); err != nil { + return fmt.Errorf("failed to unmarshal tools: %w", err) + } + } + + readOnly := viper.GetBool("read-only") + outputFormat := viper.GetString("compare-scopes-output") + + // Get API host for GitHub Enterprise support + apiHost := viper.GetString("host") + if apiHost != "" { + // Ensure it starts with https:// + if !strings.HasPrefix(apiHost, "http://") && !strings.HasPrefix(apiHost, "https://") { + apiHost = "https://" + apiHost + } + // GitHub Enterprise uses /api/v3 endpoint + if !strings.Contains(apiHost, "api.github.com") { + apiHost = strings.TrimSuffix(apiHost, "/") + "/api/v3" + } + } + + // Fetch token scopes from GitHub API + ctx := context.Background() + var tokenScopes []string + var err error + + if apiHost == "" { + tokenScopes, err = scopes.FetchTokenScopes(ctx, token) + } else { + tokenScopes, err = scopes.FetchTokenScopesWithHost(ctx, token, apiHost) + } + if err != nil { + return fmt.Errorf("failed to fetch token scopes: %w", err) + } + + // Build inventory to get required scopes + t, _ := translations.TranslationHelper() + inventoryBuilder := github.NewInventory(t). + WithReadOnly(readOnly) + + if enabledToolsets != nil { + inventoryBuilder = inventoryBuilder.WithToolsets(enabledToolsets) + } + if len(enabledTools) > 0 { + inventoryBuilder = inventoryBuilder.WithTools(enabledTools) + } + + inv := inventoryBuilder.Build() + + // Collect tool scopes + scopesOutput := collectToolScopes(inv, readOnly) + + // Compare scopes + comparison := compareScopes(tokenScopes, scopesOutput.UniqueScopes) + + // Create output structure + output := CompareOutput{ + Comparison: comparison, + EnabledToolsets: scopesOutput.EnabledToolsets, + ReadOnly: readOnly, + Tools: scopesOutput.Tools, + ScopesByTool: scopesOutput.ScopesByTool, + } + + // Output based on format + switch outputFormat { + case "json": + return outputCompareJSON(output) + default: + return outputCompareText(output) + } +} + +func compareScopes(tokenScopes, requiredScopes []string) ScopeComparison { + // Create sets for efficient lookup + tokenSet := make(map[string]bool) + for _, scope := range tokenScopes { + tokenSet[scope] = true + } + + requiredSet := make(map[string]bool) + for _, scope := range requiredScopes { + requiredSet[scope] = true + } + + // Find missing scopes (required but not in token) + var missingScopes []string + for _, scope := range requiredScopes { + // Use scope hierarchy to check if token has equivalent parent scope + if !scopes.HasRequiredScopes(tokenScopes, []string{scope}) { + missingScopes = append(missingScopes, scope) + } + } + + // Find extra scopes (in token but not covering any required scope) + // A token scope is "extra" only if: + // 1. It's not directly in the required set, AND + // 2. None of the required scopes would be satisfied by this token scope, AND + // 3. This token scope is not a child of any required scope + var extraScopes []string + for _, tokenScope := range tokenScopes { + if requiredSet[tokenScope] { + // Directly required, not extra + continue + } + + // Check if this token scope covers any required scope + coversAnyRequired := false + for _, reqScope := range requiredScopes { + // Check if tokenScope would satisfy reqScope + if scopes.HasRequiredScopes([]string{tokenScope}, []string{reqScope}) { + coversAnyRequired = true + break + } + } + + // Check if this token scope is covered by any required scope (i.e., it's a subset) + isCoveredByRequired := false + for _, reqScope := range requiredScopes { + // Check if reqScope would satisfy tokenScope + if scopes.HasRequiredScopes([]string{reqScope}, []string{tokenScope}) { + isCoveredByRequired = true + break + } + } + + // Only mark as extra if it doesn't cover any required scope AND isn't covered by any required scope + if !coversAnyRequired && !isCoveredByRequired { + extraScopes = append(extraScopes, tokenScope) + } + } + + sort.Strings(missingScopes) + sort.Strings(extraScopes) + + return ScopeComparison{ + TokenScopes: tokenScopes, + RequiredScopes: requiredScopes, + MissingScopes: missingScopes, + ExtraScopes: extraScopes, + HasAllRequired: len(missingScopes) == 0, + } +} + +func outputCompareJSON(output CompareOutput) error { + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(output) +} + +func outputCompareText(output CompareOutput) error { + fmt.Println("PAT Scope Comparison") + fmt.Println("====================") + fmt.Println() + + comparison := output.Comparison + + // Token scopes section + fmt.Println("Token Scopes:") + if len(comparison.TokenScopes) == 0 { + fmt.Println(" (no scopes - might be a fine-grained PAT)") + } else { + for _, scope := range comparison.TokenScopes { + fmt.Printf(" • %s\n", scope) + } + } + fmt.Println() + + // Required scopes section + fmt.Println("Required Scopes:") + if len(comparison.RequiredScopes) == 0 { + fmt.Println(" (no scopes required)") + } else { + for _, scope := range comparison.RequiredScopes { + fmt.Printf(" • %s\n", formatScopeDisplay(scope)) + } + } + fmt.Println() + + // Comparison result + fmt.Println("Comparison Result:") + if comparison.HasAllRequired { + fmt.Println(" ✓ Token has all required scopes!") + } else { + fmt.Println(" ✗ Token is missing required scopes") + } + fmt.Println() + + // Missing scopes + if len(comparison.MissingScopes) > 0 { + fmt.Println("Missing Scopes (need to add):") + for _, scope := range comparison.MissingScopes { + fmt.Printf(" ✗ %s\n", formatScopeDisplay(scope)) + } + fmt.Println() + } + + // Extra scopes + if len(comparison.ExtraScopes) > 0 { + fmt.Println("Extra Scopes (not required but granted):") + for _, scope := range comparison.ExtraScopes { + fmt.Printf(" • %s\n", scope) + } + fmt.Println() + } + + // Configuration info + fmt.Printf("Configuration: %d toolset(s), read-only=%v\n", len(output.EnabledToolsets), output.ReadOnly) + fmt.Printf("Toolsets: %s\n", strings.Join(output.EnabledToolsets, ", ")) + + return nil +} diff --git a/cmd/github-mcp-server/compare_scopes_test.go b/cmd/github-mcp-server/compare_scopes_test.go new file mode 100644 index 000000000..eff8dd717 --- /dev/null +++ b/cmd/github-mcp-server/compare_scopes_test.go @@ -0,0 +1,138 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCompareScopes(t *testing.T) { + tests := []struct { + name string + tokenScopes []string + requiredScopes []string + wantMissing []string + wantExtra []string + wantHasAll bool + }{ + { + name: "exact match", + tokenScopes: []string{"repo", "user"}, + requiredScopes: []string{"repo", "user"}, + wantMissing: []string{}, + wantExtra: []string{}, + wantHasAll: true, + }, + { + name: "missing scopes", + tokenScopes: []string{"user"}, + requiredScopes: []string{"repo", "user"}, + wantMissing: []string{"repo"}, + wantExtra: []string{}, + wantHasAll: false, + }, + { + name: "extra scopes", + tokenScopes: []string{"repo", "user", "gist"}, + requiredScopes: []string{"repo", "user"}, + wantMissing: []string{}, + wantExtra: []string{"gist"}, + wantHasAll: true, + }, + { + name: "missing and extra scopes", + tokenScopes: []string{"user", "gist"}, + requiredScopes: []string{"repo", "user"}, + wantMissing: []string{"repo"}, + wantExtra: []string{"gist"}, + wantHasAll: false, + }, + { + name: "empty token scopes", + tokenScopes: []string{}, + requiredScopes: []string{"repo", "user"}, + wantMissing: []string{"repo", "user"}, + wantExtra: []string{}, + wantHasAll: false, + }, + { + name: "empty required scopes", + tokenScopes: []string{"repo", "user"}, + requiredScopes: []string{}, + wantMissing: []string{}, + wantExtra: []string{"repo", "user"}, + wantHasAll: true, + }, + { + name: "both empty", + tokenScopes: []string{}, + requiredScopes: []string{}, + wantMissing: []string{}, + wantExtra: []string{}, + wantHasAll: true, + }, + { + name: "parent scope covers child scope", + tokenScopes: []string{"repo"}, + requiredScopes: []string{"public_repo"}, + wantMissing: []string{}, + wantExtra: []string{}, + wantHasAll: true, + }, + { + name: "admin:org covers read:org", + tokenScopes: []string{"admin:org"}, + requiredScopes: []string{"read:org"}, + wantMissing: []string{}, + wantExtra: []string{}, + wantHasAll: true, + }, + { + name: "child scope doesn't cover parent", + tokenScopes: []string{"public_repo"}, + requiredScopes: []string{"repo"}, + wantMissing: []string{"repo"}, + wantExtra: []string{}, + wantHasAll: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := compareScopes(tt.tokenScopes, tt.requiredScopes) + + assert.Equal(t, tt.tokenScopes, result.TokenScopes) + assert.Equal(t, tt.requiredScopes, result.RequiredScopes) + assert.Equal(t, tt.wantHasAll, result.HasAllRequired, "HasAllRequired mismatch") + + // Check missing scopes + if len(tt.wantMissing) == 0 { + assert.Empty(t, result.MissingScopes, "expected no missing scopes") + } else { + require.Equal(t, tt.wantMissing, result.MissingScopes, "missing scopes mismatch") + } + + // Check extra scopes + if len(tt.wantExtra) == 0 { + assert.Empty(t, result.ExtraScopes, "expected no extra scopes") + } else { + require.Equal(t, tt.wantExtra, result.ExtraScopes, "extra scopes mismatch") + } + }) + } +} + +func TestCompareScopes_Sorting(t *testing.T) { + // Test that missing and extra scopes are sorted + result := compareScopes( + []string{"zebra", "alpha", "beta"}, + []string{"delta", "charlie", "alpha"}, + ) + + // Missing scopes should be sorted + assert.Equal(t, []string{"charlie", "delta"}, result.MissingScopes) + + // Extra scopes should be sorted + assert.Equal(t, []string{"beta", "zebra"}, result.ExtraScopes) +} diff --git a/docs/cli-scope-commands.md b/docs/cli-scope-commands.md new file mode 100644 index 000000000..faeb7a767 --- /dev/null +++ b/docs/cli-scope-commands.md @@ -0,0 +1,175 @@ +# CLI Commands for OAuth Scope Management + +The GitHub MCP Server provides two CLI commands to help manage OAuth scopes for your Personal Access Token (PAT). + +## list-scopes + +Lists the OAuth scopes required by your enabled tools. + +### Usage + +```bash +# List scopes for default toolsets +github-mcp-server list-scopes + +# List scopes for specific toolsets +github-mcp-server list-scopes --toolsets=repos,issues,pull_requests + +# List scopes for all toolsets +github-mcp-server list-scopes --toolsets=all + +# Show only unique scopes needed (summary format) +github-mcp-server list-scopes --output=summary + +# Get JSON output for programmatic use +github-mcp-server list-scopes --output=json + +# Use the convenience script +script/list-scopes --toolsets=all --output=summary +``` + +### Example Output + +``` +Required OAuth scopes for enabled tools: + + read:org + repo + +Total: 2 unique scope(s) +``` + +## compare-scopes + +Compares your PAT's granted scopes with the scopes required by enabled tools. + +### Usage + +```bash +# Export your token +export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_yourtoken + +# Compare with default toolsets +github-mcp-server compare-scopes + +# Compare with specific toolsets +github-mcp-server compare-scopes --toolsets=repos,issues,pull_requests + +# Provide token via flag +github-mcp-server compare-scopes --token=ghp_yourtoken + +# Get JSON output +github-mcp-server compare-scopes --output=json + +# Use the convenience script +script/compare-scopes --toolsets=all +``` + +### Example Output + +When all scopes are present: +``` +PAT Scope Comparison +==================== + +Token Scopes: + • repo + • read:org + +Required Scopes: + • read:org + • repo + +Comparison Result: + ✓ Token has all required scopes! + +Configuration: 5 toolset(s), read-only=false +Toolsets: context, issues, pull_requests, repos, users +``` + +When scopes are missing: +``` +PAT Scope Comparison +==================== + +Token Scopes: + • read:org + +Required Scopes: + • read:org + • repo + +Comparison Result: + ✗ Token is missing required scopes + +Missing Scopes (need to add): + ✗ repo + +Configuration: 5 toolset(s), read-only=false +Toolsets: context, issues, pull_requests, repos, users +``` + +## Workflow: Setting Up a New Token + +Use these commands together to set up a new PAT: + +1. **Determine required scopes** + ```bash + script/list-scopes --toolsets=repos,issues,pull_requests --output=summary + ``` + +2. **Create PAT on GitHub** with the listed scopes + +3. **Verify the token** + ```bash + export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_newtoken + script/compare-scopes --toolsets=repos,issues,pull_requests + ``` + +4. **If scopes are missing**, update your PAT on GitHub and verify again + +## Features + +### Scope Hierarchy Support + +Both commands understand GitHub's OAuth scope hierarchy: +- `repo` grants `public_repo` and `security_events` +- `admin:org` grants `write:org` and `read:org` +- `write:org` grants `read:org` +- `project` grants `read:project` +- `write:packages` grants `read:packages` +- `user` grants `read:user` and `user:email` + +### GitHub Enterprise Support + +Both commands support GitHub Enterprise Server: + +```bash +# For GitHub Enterprise Server +github-mcp-server list-scopes --gh-host=github.enterprise.com +github-mcp-server compare-scopes --gh-host=github.enterprise.com +``` + +### Fine-Grained PATs + +Fine-grained Personal Access Tokens use a different permission model and don't return OAuth scopes. The `compare-scopes` command will detect this: + +``` +Token Scopes: + (no scopes - might be a fine-grained PAT) +``` + +Fine-grained PATs cannot be validated with these commands. Use classic PATs (starting with `ghp_`) for OAuth scope validation. + +## Additional Options + +Both commands support the same configuration flags as the `stdio` command: + +- `--toolsets`: Specify which toolsets to include +- `--tools`: Specify individual tools to include +- `--read-only`: Restrict to read-only operations (reduces required scopes) +- `--gh-host`: Use GitHub Enterprise Server + +For complete documentation: +- `list-scopes`: See command help with `github-mcp-server list-scopes --help` +- `compare-scopes`: See [docs/compare-scopes.md](docs/compare-scopes.md) for detailed guide diff --git a/docs/compare-scopes.md b/docs/compare-scopes.md new file mode 100644 index 000000000..6384407e2 --- /dev/null +++ b/docs/compare-scopes.md @@ -0,0 +1,239 @@ +# Compare PAT Scopes Command + +The `compare-scopes` command helps you verify that your GitHub Personal Access Token (PAT) has the necessary OAuth scopes for the tools you want to use. + +## Overview + +This command: +1. Fetches the OAuth scopes granted to your PAT from the GitHub API +2. Determines the scopes required by your enabled tools (based on toolset configuration) +3. Compares the two and reports any missing or extra scopes + +## Usage + +### Basic Usage + +```bash +# Export your token +export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_yourtoken + +# Compare scopes with default toolsets +github-mcp-server compare-scopes + +# Or use the convenience script +script/compare-scopes +``` + +### With Specific Toolsets + +```bash +# Compare for specific toolsets +github-mcp-server compare-scopes --toolsets=repos,issues,pull_requests + +# Compare with all toolsets +github-mcp-server compare-scopes --toolsets=all + +# Compare with read-only mode +github-mcp-server compare-scopes --read-only +``` + +### Token via Flag + +```bash +# Provide token via flag (overrides environment variable) +github-mcp-server compare-scopes --token=ghp_yourtoken +``` + +### JSON Output + +```bash +# Get JSON output for programmatic use +github-mcp-server compare-scopes --output=json +``` + +## Output Formats + +### Text Output (Default) + +The default text output provides a human-readable comparison: + +``` +PAT Scope Comparison +==================== + +Token Scopes: + • repo + • read:org + +Required Scopes: + • read:org + • repo + +Comparison Result: + ✓ Token has all required scopes! + +Configuration: 5 toolset(s), read-only=false +Toolsets: context, issues, pull_requests, repos, users +``` + +When scopes are missing: + +``` +PAT Scope Comparison +==================== + +Token Scopes: + • read:org + +Required Scopes: + • read:org + • repo + +Comparison Result: + ✗ Token is missing required scopes + +Missing Scopes (need to add): + ✗ repo + +Configuration: 5 toolset(s), read-only=false +Toolsets: context, issues, pull_requests, repos, users +``` + +### JSON Output + +Use `--output=json` for machine-readable output: + +```json +{ + "comparison": { + "token_scopes": ["repo", "read:org"], + "required_scopes": ["read:org", "repo"], + "missing_scopes": [], + "extra_scopes": [], + "has_all_required": true + }, + "enabled_toolsets": ["context", "issues", "pull_requests", "repos", "users"], + "read_only": false, + "tools": [...], + "scopes_by_tool": {...} +} +``` + +## Scope Hierarchy + +The command understands GitHub's OAuth scope hierarchy. For example: +- `repo` grants access to `public_repo` and `security_events` +- `admin:org` grants access to `write:org` and `read:org` +- `write:org` grants access to `read:org` + +If your token has a parent scope, the command will recognize that child scopes are satisfied. + +## Fine-Grained PATs + +Fine-grained Personal Access Tokens (PATs) don't return the `X-OAuth-Scopes` header, so the command will report: + +``` +Token Scopes: + (no scopes - might be a fine-grained PAT) +``` + +Fine-grained PATs use a different permission model and cannot be validated with this command. + +## GitHub Enterprise Support + +The command supports GitHub Enterprise Server: + +```bash +# For GitHub Enterprise Server +github-mcp-server compare-scopes --gh-host=github.enterprise.com +``` + +## Common Scenarios + +### Creating a New Token + +When creating a new token, use this command to verify you've selected all necessary scopes: + +```bash +# Check what scopes you need +github-mcp-server list-scopes --output=summary + +# After creating the token, verify it has everything +export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_newtoken +github-mcp-server compare-scopes +``` + +### Debugging Permission Issues + +If you're getting permission errors when using the server: + +```bash +# Check if your token is missing required scopes +github-mcp-server compare-scopes --toolsets=all +``` + +### Different Toolset Configurations + +Compare scopes for different configurations: + +```bash +# Minimal configuration +github-mcp-server compare-scopes --toolsets=repos,issues + +# Full configuration +github-mcp-server compare-scopes --toolsets=all + +# Read-only mode (fewer permissions needed) +github-mcp-server compare-scopes --read-only --toolsets=all +``` + +## Exit Codes + +- `0`: Success (command ran successfully) +- `1`: Error (missing token, API error, or other failure) + +Note: The exit code does NOT indicate whether scopes match - check the output to determine if scopes are missing. + +## Related Commands + +- `list-scopes`: Lists required OAuth scopes for enabled tools (without checking your token) +- `stdio`: Runs the MCP server with your configured toolsets + +## Example Workflows + +### Workflow 1: Setting Up a New Token + +1. List required scopes: + ```bash + script/list-scopes --toolsets=repos,issues,pull_requests --output=summary + ``` + +2. Create a PAT on GitHub with those scopes + +3. Verify the token: + ```bash + export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_newtoken + script/compare-scopes --toolsets=repos,issues,pull_requests + ``` + +### Workflow 2: Troubleshooting Access Issues + +1. Check current token scopes: + ```bash + script/compare-scopes --toolsets=all + ``` + +2. If scopes are missing, update your PAT on GitHub + +3. Verify the update: + ```bash + script/compare-scopes --toolsets=all + ``` + +## Notes + +- Classic PATs (starting with `ghp_`) are fully supported +- Fine-grained PATs are detected but cannot be validated +- The command uses a HEAD request to minimize bandwidth +- Scope hierarchy is automatically handled +- The `--read-only` flag reduces required permissions diff --git a/script/compare-scopes b/script/compare-scopes new file mode 100755 index 000000000..9e1db817e --- /dev/null +++ b/script/compare-scopes @@ -0,0 +1,29 @@ +#!/bin/bash +# +# Compare PAT token scopes with required scopes for enabled tools. +# +# Usage: +# script/compare-scopes [--toolsets=...] [--token=...] [--output=text|json] +# +# The token can be provided via GITHUB_PERSONAL_ACCESS_TOKEN environment variable +# or via the --token flag. +# +# Examples: +# export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_... +# script/compare-scopes +# +# script/compare-scopes --toolsets=all --output=json +# script/compare-scopes --token=ghp_... --toolsets=repos,issues +# + +set -e + +cd "$(dirname "$0")/.." + +# Build the server if it doesn't exist or is outdated +if [ ! -f github-mcp-server ] || [ cmd/github-mcp-server/compare_scopes.go -nt github-mcp-server ]; then + echo "Building github-mcp-server..." >&2 + go build -o github-mcp-server ./cmd/github-mcp-server +fi + +exec ./github-mcp-server compare-scopes "$@"