package scopes import ( "slices" "sort" ) // Scope represents a GitHub OAuth scope. // These constants define all OAuth scopes used by the GitHub MCP server tools. // See https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps type Scope string const ( // NoScope indicates no scope is required (public access). NoScope Scope = "" // Repo grants full control of private repositories Repo Scope = "repo" // PublicRepo grants access to public repositories PublicRepo Scope = "public_repo" // ReadOrg grants read-only access to organization membership, teams, and projects ReadOrg Scope = "read:org" // WriteOrg grants write access to organization membership and teams WriteOrg Scope = "write:org" // AdminOrg grants full control of organizations and teams AdminOrg Scope = "admin:org" // Gist grants write access to gists Gist Scope = "gist" // Notifications grants access to notifications Notifications Scope = "notifications" // ReadProject grants read-only access to projects ReadProject Scope = "read:project" // Project grants full control of projects Project Scope = "project" // SecurityEvents grants read and write access to security events SecurityEvents Scope = "security_events" // User grants read/write access to profile info User Scope = "user" // ReadUser grants read-only access to profile info ReadUser Scope = "read:user" // UserEmail grants read access to user email addresses UserEmail Scope = "user:email" // ReadPackages grants read access to packages ReadPackages Scope = "read:packages" // WritePackages grants write access to packages WritePackages Scope = "write:packages" ) // ScopeHierarchy defines parent-child relationships between scopes. // A parent scope implicitly grants access to all child scopes. // For example, "repo" grants access to "public_repo" and "security_events". var ScopeHierarchy = map[Scope][]Scope{ Repo: {PublicRepo, SecurityEvents}, AdminOrg: {WriteOrg, ReadOrg}, WriteOrg: {ReadOrg}, Project: {ReadProject}, WritePackages: {ReadPackages}, User: {ReadUser, UserEmail}, } // ScopeSet represents a set of OAuth scopes. type ScopeSet map[Scope]bool // NewScopeSet creates a new ScopeSet from the given scopes. func NewScopeSet(scopes ...Scope) ScopeSet { set := make(ScopeSet) for _, scope := range scopes { set[scope] = true } return set } // ToSlice converts a ScopeSet to a slice of Scope values. func (s ScopeSet) ToSlice() []Scope { scopes := make([]Scope, 0, len(s)) for scope := range s { scopes = append(scopes, scope) } // Sort for deterministic output slices.Sort(scopes) return scopes } // ToStringSlice converts a ScopeSet to a slice of string values. // The returned slice is sorted for deterministic output. func (s ScopeSet) ToStringSlice() []string { scopes := make([]string, 0, len(s)) for scope := range s { scopes = append(scopes, string(scope)) } sort.Strings(scopes) return scopes } // ToStringSlice converts a slice of Scopes to a slice of strings. func ToStringSlice(scopes ...Scope) []string { result := make([]string, len(scopes)) for i, scope := range scopes { result[i] = string(scope) } return result } // ExpandScopes takes a list of required scopes and returns all accepted scopes // including parent scopes from the hierarchy. // For example, if "public_repo" is required, "repo" is also accepted since // having the "repo" scope grants access to "public_repo". // The returned slice is sorted for deterministic output. func ExpandScopes(required ...Scope) []string { if len(required) == 0 { return nil } accepted := make(map[string]bool) // Add required scopes for _, scope := range required { accepted[string(scope)] = true } // Add parent scopes that grant access to required scopes for parent, children := range ScopeHierarchy { for _, child := range children { if accepted[string(child)] { accepted[string(parent)] = true } } } // Convert to slice and sort for deterministic output result := make([]string, 0, len(accepted)) for scope := range accepted { result = append(result, scope) } sort.Strings(result) return result } // expandScopeSet returns a set of all scopes granted by the given scopes, // including child scopes from the hierarchy. // For example, if "repo" is provided, the result includes "repo", "public_repo", // and "security_events" since "repo" grants access to those child scopes. func expandScopeSet(scopes []string) map[string]bool { expanded := make(map[string]bool, len(scopes)) for _, scope := range scopes { expanded[scope] = true // Add child scopes granted by this scope if children, ok := ScopeHierarchy[Scope(scope)]; ok { for _, child := range children { expanded[string(child)] = true } } } return expanded } // HasRequiredScopes checks if tokenScopes satisfy the acceptedScopes requirement. // A tool's acceptedScopes includes both the required scopes AND parent scopes // that implicitly grant the required permissions (via ExpandScopes). // // For PAT filtering: if ANY of the acceptedScopes are granted by the token // (directly or via scope hierarchy), the tool should be visible. // // Returns true if the tool should be visible to the token holder. func HasRequiredScopes(tokenScopes []string, acceptedScopes []string) bool { // No scopes required = always allowed if len(acceptedScopes) == 0 { return true } // Expand token scopes to include child scopes they grant grantedScopes := expandScopeSet(tokenScopes) // Check if any accepted scope is granted by the token for _, accepted := range acceptedScopes { if grantedScopes[accepted] { return true } } return false }