11package main
22
33import (
4+ "context"
45 "errors"
56 "fmt"
67 "os"
8+ "sort"
79 "strings"
810 "time"
911
1012 "github.com/github/github-mcp-server/internal/ghmcp"
1113 "github.com/github/github-mcp-server/internal/oauth"
1214 "github.com/github/github-mcp-server/pkg/github"
15+ "github.com/github/github-mcp-server/pkg/inventory"
16+ "github.com/github/github-mcp-server/pkg/translations"
1317 "github.com/spf13/cobra"
1418 "github.com/spf13/pflag"
1519 "github.com/spf13/viper"
@@ -187,18 +191,70 @@ func wordSepNormalizeFunc(_ *pflag.FlagSet, name string) pflag.NormalizedName {
187191 return pflag .NormalizedName (name )
188192}
189193
190- // getOAuthScopes returns the OAuth scopes to request
191- // Uses custom scopes if provided, otherwise defaults to common scopes
194+ // getOAuthScopes returns the OAuth scopes to request based on enabled tools
195+ // Uses custom scopes if explicitly provided, otherwise computes required scopes
196+ // from the tools that will be enabled based on user configuration
192197func getOAuthScopes () []string {
198+ // Allow explicit override via --oauth-scopes flag
193199 var scopes []string
194200 if viper .IsSet ("oauth_scopes" ) {
195201 if err := viper .UnmarshalKey ("oauth_scopes" , & scopes ); err == nil && len (scopes ) > 0 {
196202 return scopes
197203 }
198204 }
199205
200- // Default scopes for GitHub MCP Server
201- // Based on the protected resource metadata at https://api.githubcopilot.com/.well-known/oauth-protected-resource/mcp
206+ // Compute required scopes based on enabled tools
207+ // This ensures we only request scopes for tools the user will actually use
208+ var enabledToolsets []string
209+ if viper .IsSet ("toolsets" ) {
210+ if err := viper .UnmarshalKey ("toolsets" , & enabledToolsets ); err != nil {
211+ // If unmarshaling fails, fall back to defaults
212+ enabledToolsets = nil
213+ }
214+ }
215+
216+ var enabledTools []string
217+ if viper .IsSet ("tools" ) {
218+ if err := viper .UnmarshalKey ("tools" , & enabledTools ); err != nil {
219+ enabledTools = nil
220+ }
221+ }
222+
223+ var enabledFeatures []string
224+ if viper .IsSet ("features" ) {
225+ if err := viper .UnmarshalKey ("features" , & enabledFeatures ); err != nil {
226+ enabledFeatures = nil
227+ }
228+ }
229+
230+ // Build inventory with the same configuration that will be used at runtime
231+ // This allows us to determine which tools will actually be available
232+ t , _ := translations .TranslationHelper ()
233+ inventoryBuilder := github .NewInventory (t ).
234+ WithReadOnly (viper .GetBool ("read-only" )).
235+ WithToolsets (enabledToolsets ).
236+ WithTools (enabledTools ).
237+ WithFeatureChecker (createFeatureChecker (enabledFeatures ))
238+
239+ inventory , err := inventoryBuilder .Build ()
240+ if err != nil {
241+ // If inventory build fails, fall back to default scopes
242+ return getDefaultOAuthScopes ()
243+ }
244+
245+ // Collect all required scopes from available tools
246+ requiredScopes := collectRequiredScopes (inventory )
247+ if len (requiredScopes ) == 0 {
248+ // If no tools require scopes, use defaults
249+ return getDefaultOAuthScopes ()
250+ }
251+
252+ return requiredScopes
253+ }
254+
255+ // getDefaultOAuthScopes returns the default scopes for GitHub MCP Server
256+ // Based on the protected resource metadata at https://api.githubcopilot.com/.well-known/oauth-protected-resource/mcp
257+ func getDefaultOAuthScopes () []string {
202258 return []string {
203259 "repo" ,
204260 "user" ,
@@ -208,3 +264,39 @@ func getOAuthScopes() []string {
208264 "project" ,
209265 }
210266}
267+
268+ // collectRequiredScopes collects all unique required scopes from available tools
269+ // Returns a sorted, deduplicated list of OAuth scopes needed for the enabled tools
270+ func collectRequiredScopes (inv * inventory.Inventory ) []string {
271+ scopeSet := make (map [string ]bool )
272+
273+ // Get available tools (respects filters like read-only, toolsets, etc.)
274+ for _ , tool := range inv .AvailableTools (context .Background ()) {
275+ for _ , scope := range tool .RequiredScopes {
276+ if scope != "" {
277+ scopeSet [scope ] = true
278+ }
279+ }
280+ }
281+
282+ // Convert to sorted slice for deterministic output
283+ scopes := make ([]string , 0 , len (scopeSet ))
284+ for scope := range scopeSet {
285+ scopes = append (scopes , scope )
286+ }
287+ sort .Strings (scopes )
288+
289+ return scopes
290+ }
291+
292+ // createFeatureChecker creates a feature flag checker from enabled features list
293+ func createFeatureChecker (enabledFeatures []string ) inventory.FeatureFlagChecker {
294+ // Build a set for O(1) lookup
295+ featureSet := make (map [string ]bool , len (enabledFeatures ))
296+ for _ , f := range enabledFeatures {
297+ featureSet [f ] = true
298+ }
299+ return func (_ context.Context , flagName string ) (bool , error ) {
300+ return featureSet [flagName ], nil
301+ }
302+ }
0 commit comments