Skip to content

feat: Add OAuth scopes to tool metadata (Phase 1) - #1485

Closed
SamMorrowDrums wants to merge 7 commits into
mainfrom
SamMorrowDrums/oauth-scopes
Closed

feat: Add OAuth scopes to tool metadata (Phase 1)#1485
SamMorrowDrums wants to merge 7 commits into
mainfrom
SamMorrowDrums/oauth-scopes

Conversation

@SamMorrowDrums

Copy link
Copy Markdown
Collaborator

Summary

This PR adds OAuth scope metadata to all MCP tools, enabling clients to know which scopes are required for each tool before calling them.

Changes

New Package: pkg/scopes

  • scopes.go: OAuth scope constants based on GitHub's OAuth app scopes documentation

    • Scope type with constants for all GitHub OAuth scopes (Repo, PublicRepo, Notifications, Gist, SecurityEvents, Project, ReadProject, ReadOrg, etc.)
    • ScopeHierarchy map defining parent-child relationships (e.g., repo includes public_repo, security_events, etc.)
    • WithScopes() helper to create mcp.Tool.Meta maps
    • GetScopesFromMeta() to extract scopes from tool metadata
    • ScopeIncludes(), HasRequiredScopes() for scope checking
  • scopes_test.go: Comprehensive test coverage for all utilities

Tool Updates (~90 tools)

Added Meta: scopes.WithScopes(...) to all tool definitions:

ScopeTools
repoMost repository, issue, PR, actions, discussions, search tools
public_repostar_repository, unstar_repository
notificationsAll notification tools
gistcreate_gist, update_gist
security_eventsCode scanning, dependabot, secret scanning, security advisories
projectProject write operations
read:projectProject read operations
read:orgget_teams, get_team_members, list_issue_types
No scopeget_me, list_gists, get_gist (public reads)

Documentation Updates

  • generate_docs.go: Updated to include scope information in README output
  • README.md: Now shows (scopes: \repo`)` after each tool description
  • Toolsnaps: All updated with _meta.requiredOAuthScopes array

Testing

  • All existing tests pass
  • New test file pkg/scopes/scopes_test.go with tests for:
    • TestScopeString - Scope string conversion
    • TestScopeIncludes - Hierarchy checking
    • TestHasRequiredScopes - Multiple scope validation
    • TestWithScopes - Meta map creation
    • TestGetScopesFromMeta - Meta extraction
    • TestGetAcceptedScopes - Parent scope lookup
    • TestScopeStringsAndParseScopes - Round-trip conversion

Part of OAuth Scopes Work (Phase 1 of 4)

  • Phase 1 (this PR): Add OAuth scopes to tool metadata ✅
  • Phase 2: Add fine-grained permissions to metadata
  • Phase 3: Create script to list required scopes for enabled tools
  • Phase 4: Export Go map for library usage

CopilotAI review requested due to automatic review settings November 25, 2025 13:00
@SamMorrowDrums
SamMorrowDrums requested a review from a team as a code ownerNovember 25, 2025 13:00

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements Phase 1 of OAuth scope support by adding scope metadata to all MCP tools, enabling clients to determine required permissions before making tool calls.

Key Changes:

  • New pkg/scopes package with OAuth scope constants, hierarchy definitions, and utility functions for scope validation
  • Added Meta: scopes.WithScopes(...) to ~90 MCP tools across the codebase with appropriate scope assignments
  • Updated documentation generation to include scope information in README
  • All toolsnaps updated with _meta.requiredOAuthScopes field

Reviewed changes

Copilot reviewed 113 out of 113 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
pkg/scopes/scopes.goNew package defining OAuth scopes, hierarchy, and utility functions (MetaKey, WithScopes, GetScopesFromMeta, ScopeIncludes, HasRequiredScopes, GetAcceptedScopes)
pkg/scopes/scopes_test.goComprehensive test coverage for all scope utilities including hierarchy checks and metadata handling
pkg/github/*.go (multiple files)Added scope metadata to tools - repo for most operations, security_events for security features, notifications for notification tools, gist for gist writes, read:org/project for reads, NoScope for public access
pkg/github/toolsnaps/*.snap (many files)Added _meta.requiredOAuthScopes arrays to all tool snapshots
cmd/github-mcp-server/generate_docs.goEnhanced doc generation to append scope information (e.g., "(scopes: `repo`)") to tool descriptions
README.mdUpdated with scope annotations for all tools in documentation

Comment threadpkg/scopes/scopes.go
Comment on lines +153 to +176
// GetAcceptedScopes returns all scopes that satisfy the given required scope.
// This includes the scope itself plus any parent scopes that include it.
func GetAcceptedScopes(required Scope) []Scope {
accepted := []Scope{required}

// Find all scopes that include the required scope
for parent, children := range ScopeHierarchy {
for _, child := range children {
if child == required {
accepted = append(accepted, parent)
// Also check if this parent is included by another scope
for grandparent, grandchildren := range ScopeHierarchy {
for _, gc := range grandchildren {
if gc == parent {
accepted = append(accepted, grandparent)
}
}
}
}
}
}

return accepted
}

CopilotAINov 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The GetAcceptedScopes function only handles up to 2 levels of hierarchy (parent and grandparent) using nested loops, while ScopeIncludes properly handles arbitrary depth using recursion. Although this works for the current GitHub OAuth scope hierarchy (which has at most 3 levels), consider refactoring to use recursion for consistency and future-proofing:

funcGetAcceptedScopes(requiredScope) []Scope {
accepted:= []Scope{required}
seen:=make(map[Scope]bool)
seen[required] =true// Recursively find all parent scopesvarfindParentsfunc(Scope)
findParents=func(childScope) {
forparent, children:=rangeScopeHierarchy {
for_, c:=rangechildren {
ifc==child&&!seen[parent] {
seen[parent] =trueaccepted=append(accepted, parent)
findParents(parent) // Recursively find parents of this parent
}
}
}
}
findParents(required)
returnaccepted
}

This approach would handle hierarchies of any depth and avoid potential issues if the scope hierarchy is extended in the future.

Copilot uses AI. Check for mistakes.
- Add pkg/scopes package with OAuth scope constants and utilities
- Add scopes to all ~90 tool definitions using mcp.Tool.Meta field
- Update generate_docs.go to include scopes in README output
- Add comprehensive test coverage for scopes package
- Update all toolsnaps to include _meta.requiredOAuthScopes
Scope assignments follow GitHub OAuth app documentation:
- repo: Most repository operations (private repos)
- public_repo: Star/unstar operations
- notifications: Notification operations
- gist: Gist write operations
- security_events: Code scanning, dependabot, secret scanning
- project/read:project: Project operations
- read:org: Organization/team read operations
- No scope: Public read operations (get_me, list_gists)
@SamMorrowDrums
SamMorrowDrumsforce-pushed the SamMorrowDrums/oauth-scopes branch from ee31dc6 to c6f31f3CompareDecember 13, 2025 00:22
Add a new constructor that creates a ToolsetGroup from a list of ServerTools.
Tools are automatically categorized as read or write based on their
ReadOnlyHint annotation, and grouped into toolsets based on the 'toolset'
field in their Meta.
This enables tools to be fully self-describing - registration can now
derive toolset membership and read/write classification directly from
the tool definition.
…roupFromTools
- Move ToolsetMetadata type to toolsets package with alias in github package
- Update NewToolMeta to accept ToolsetMetadata instead of string
- Update NewToolsetGroupFromTools to accept []ToolsetMetadata slice
- Remove need for .ID when calling NewToolMeta (e.g., NewToolMeta(ToolsetMetadataRepos) instead of NewToolMeta(ToolsetMetadataRepos.ID))
Add ToolsetRegistry struct that holds toolset definitions and tools,
with a NewToolsetGroup method that accepts ToolsetGroupConfig:
- ReadOnly: restricts to read-only tools
- ActiveToolsets: which toolsets to enable
- AvailableScopes: filter tools by OAuth scope requirements
- nil: no scope checking (all tools available)
- []: empty means no scopes (only tools with no requirements)
- [...]: tools must have required scopes in the list
This enables dynamic tool filtering based on the user's actual
OAuth token scopes, ensuring only authorized tools are exposed.
Refactor DefaultToolsetGroup to use the new ToolsetRegistry pattern:
- Add NewDefaultToolsetRegistry() that creates a registry with all tools
- Tools are now self-describing (toolset + read/write from metadata)
- No more manual AddReadTools/AddWriteTools categorization
- DefaultToolsetGroup uses the registry with ToolsetGroupConfig
- Resource templates and prompts handled separately (not tools)
This enables scope-aware tool filtering when creating ToolsetGroups.
The function returns toolset metadata, not tools. Renamed for clarity
and simplified the toolset collection code per staticcheck suggestion.
- Add Meta field with toolset info to all resource templates and prompts
- Create NewResourceMeta and NewPromptMeta helpers
- Extend ToolsetRegistry to accept resources and prompts via WithResourceTemplates/WithPrompts
- Registry now automatically distributes resources/prompts to correct toolsets
- Remove addResourceTemplatesAndPrompts helper (no longer needed)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@SamMorrowDrums