feat: Add list-scopes command to show required OAuth scopes#1487
feat: Add list-scopes command to show required OAuth scopes#1487SamMorrowDrums wants to merge 1 commit intoSamMorrowDrums/oauth-scopes-phase2from
Conversation
Adds a new 'list-scopes' subcommand that outputs the required OAuth scopes for all enabled tools. This helps users determine what scopes their token needs to use specific tools. Features: - Respects all toolset configuration flags (--toolsets, --read-only) - Three output formats: text (default), json, summary - JSON output includes tools, unique_scopes, scopes_by_tool, tools_by_scope - Calculates accepted scopes (parent scopes that satisfy requirements) - Includes convenience wrapper script at script/list-scopes Usage examples: github-mcp-server list-scopes github-mcp-server list-scopes --toolsets=all --output=json github-mcp-server list-scopes --read-only --output=summary
There was a problem hiding this comment.
Pull request overview
This PR adds a new list-scopes subcommand to help users determine which OAuth scopes their GitHub token needs to use specific tools. The command respects all toolset configuration flags and supports multiple output formats (text, json, summary).
Key changes:
- New CLI command that analyzes enabled toolsets and reports required OAuth scopes
- Calculates both required scopes and accepted parent scopes that satisfy requirements
- Three output formats for different use cases (human-readable, programmatic, and concise)
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| script/list-scopes | Shell wrapper script that builds and executes the list-scopes command with argument forwarding |
| cmd/github-mcp-server/list_scopes.go | Main implementation of the list-scopes subcommand including command definition, scope collection logic, and output formatting functions |
| // Process enabled toolsets (same logic as server.go) | ||
| // If "all" is present, override all other toolsets | ||
| if github.ContainsToolset(enabledToolsets, github.ToolsetMetadataAll.ID) { | ||
| enabledToolsets = []string{github.ToolsetMetadataAll.ID} | ||
| } | ||
| // If "default" is present, expand to real toolset IDs | ||
| if github.ContainsToolset(enabledToolsets, github.ToolsetMetadataDefault.ID) { | ||
| enabledToolsets = github.AddDefaultToolset(enabledToolsets) | ||
| } |
There was a problem hiding this comment.
The toolset processing logic is missing a call to github.CleanToolsets() that exists in the stdio command (see internal/ghmcp/server.go line 102). This function:
- Removes whitespace from toolset names
- Removes duplicates
- Validates toolset names and identifies invalid ones
Without this, the command may behave inconsistently with the stdio command when users provide invalid or malformed toolset names. Add the CleanToolsets call before processing special keywords:
// Clean up the passed toolsets
enabledToolsets, invalidToolsets := github.CleanToolsets(enabledToolsets)
// Optionally warn about invalid toolsets (like server.go does)
if len(invalidToolsets) > 0 {
fmt.Fprintf(os.Stderr, "Invalid toolsets ignored: %s\n", strings.Join(invalidToolsets, ", "))
}
// If "all" is present, override all other toolsets
if github.ContainsToolset(enabledToolsets, github.ToolsetMetadataAll.ID) {
enabledToolsets = []string{github.ToolsetMetadataAll.ID}
}
// ...| acceptedScopeStrs := []string{} | ||
| for _, reqScope := range requiredScopes { | ||
| accepted := scopes.GetAcceptedScopes(reqScope) | ||
| for _, accScope := range accepted { | ||
| if accScope != reqScope { // Don't duplicate the required scope | ||
| accStr := accScope.String() | ||
| // Avoid duplicates | ||
| found := false | ||
| for _, existing := range acceptedScopeStrs { | ||
| if existing == accStr { | ||
| found = true | ||
| break | ||
| } | ||
| } | ||
| if !found { | ||
| acceptedScopeStrs = append(acceptedScopeStrs, accStr) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The manual deduplication of acceptedScopeStrs using nested loops is O(n²) and could be simplified for better maintainability and performance. Consider using a map for deduplication:
// Calculate accepted scopes (scopes that also satisfy the requirement due to hierarchy)
acceptedScopeMap := make(map[string]bool)
for _, reqScope := range requiredScopes {
accepted := scopes.GetAcceptedScopes(reqScope)
for _, accScope := range accepted {
if accScope != reqScope { // Don't duplicate the required scope
acceptedScopeMap[accScope.String()] = true
}
}
}
acceptedScopeStrs := make([]string, 0, len(acceptedScopeMap))
for scope := range acceptedScopeMap {
acceptedScopeStrs = append(acceptedScopeStrs, scope)
}
sort.Strings(acceptedScopeStrs)This approach is more efficient and clearer in intent.
| acceptedScopeStrs := []string{} | |
| for _, reqScope := range requiredScopes { | |
| accepted := scopes.GetAcceptedScopes(reqScope) | |
| for _, accScope := range accepted { | |
| if accScope != reqScope { // Don't duplicate the required scope | |
| accStr := accScope.String() | |
| // Avoid duplicates | |
| found := false | |
| for _, existing := range acceptedScopeStrs { | |
| if existing == accStr { | |
| found = true | |
| break | |
| } | |
| } | |
| if !found { | |
| acceptedScopeStrs = append(acceptedScopeStrs, accStr) | |
| } | |
| } | |
| } | |
| } | |
| // Calculate accepted scopes (scopes that also satisfy the requirement due to hierarchy) | |
| acceptedScopeMap := make(map[string]bool) | |
| for _, reqScope := range requiredScopes { | |
| accepted := scopes.GetAcceptedScopes(reqScope) | |
| for _, accScope := range accepted { | |
| if accScope != reqScope { // Don't duplicate the required scope | |
| acceptedScopeMap[accScope.String()] = true | |
| } | |
| } | |
| } | |
| acceptedScopeStrs := make([]string, 0, len(acceptedScopeMap)) | |
| for scope := range acceptedScopeMap { | |
| acceptedScopeStrs = append(acceptedScopeStrs, scope) | |
| } |
| # Build the server if it doesn't exist or is outdated | ||
| if [ ! -f github-mcp-server ] || [ cmd/github-mcp-server/list_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 list-scopes "$@" |
There was a problem hiding this comment.
[nitpick] The conditional build check may not detect all necessary rebuilds (e.g., when dependencies in other packages change). For consistency with other scripts like script/generate-docs, consider using go run instead:
exec go run ./cmd/github-mcp-server list-scopes "$@"This approach is simpler, always ensures up-to-date code execution, and aligns with the project's script patterns. The build time overhead is minimal (~1s according to the guidelines).
| # Build the server if it doesn't exist or is outdated | |
| if [ ! -f github-mcp-server ] || [ cmd/github-mcp-server/list_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 list-scopes "$@" | |
| # Always run the latest code, consistent with other scripts | |
| exec go run ./cmd/github-mcp-server list-scopes "$@" |
|
Note: By default (without $ ./github-mcp-server list-scopes --output=summary
Required OAuth scopes for enabled tools:
(no scope required for public read access)
read:org
repo
Total: 3 unique scope(s)Compare this to all toolsets: $ ./github-mcp-server list-scopes --toolsets=all --output=summary
Required OAuth scopes for enabled tools:
(no scope required for public read access)
gist
notifications
project
public_repo
read:org
read:project
repo
security_events
Total: 9 unique scope(s)This matches the same default behavior as the |
Summary
Adds a new
list-scopessubcommand that outputs the required OAuth scopes for all enabled tools. This helps users determine what scopes their token needs to use specific tools.Part 3 of the OAuth scopes work:
Changes
cmd/github-mcp-server/list_scopes.go- new subcommandscript/list-scopes- convenience wrapper scriptFeatures
--toolsets,--read-only)text(default),json,summaryUsage Examples
Example Output
Testing
script/lint- 0 issuesscript/test- All tests pass