Skip to content

Commit 5e50dfd

Browse files
Compute OAuth scopes dynamically based on enabled tools
- Replace hardcoded default scopes with dynamic scope computation - Collect RequiredScopes from tools that will be enabled based on user config - Respect --toolsets, --tools, --read-only, and --features flags - Still allow explicit override via --oauth-scopes flag - Only request minimum scopes needed for selected tools - Improves security by requesting least privilege scopes - Addresses feedback in review comment 2702294613 Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
1 parent 76defa8 commit 5e50dfd

File tree

1 file changed

+96
-4
lines changed

1 file changed

+96
-4
lines changed

cmd/github-mcp-server/main.go

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package main
22

33
import (
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
192197
func 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

Comments
 (0)