Skip to content

Commit 669690b

Browse files
Add scopes package and update ServerTool struct with scope fields
- Created pkg/scopes package with OAuth scope constants - Added RequiredScopes and AcceptedScopes fields to ServerTool - Added NewToolWithScopes helpers in dependencies.go - Updated context tools (get_me, get_teams, get_team_members) with scopes Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
1 parent 84f0ec9 commit 669690b

File tree

4 files changed

+132
-3
lines changed

4 files changed

+132
-3
lines changed

pkg/github/context_tools.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
ghErrors "github.com/github/github-mcp-server/pkg/errors"
99
"github.com/github/github-mcp-server/pkg/inventory"
10+
"github.com/github/github-mcp-server/pkg/scopes"
1011
"github.com/github/github-mcp-server/pkg/translations"
1112
"github.com/github/github-mcp-server/pkg/utils"
1213
"github.com/google/jsonschema-go/jsonschema"
@@ -38,7 +39,7 @@ type UserDetails struct {
3839

3940
// GetMe creates a tool to get details of the authenticated user.
4041
func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
41-
return NewTool(
42+
return NewToolWithScopes(
4243
ToolsetMetadataContext,
4344
mcp.Tool{
4445
Name: "get_me",
@@ -51,6 +52,8 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
5152
// OpenAI strict mode requires the properties field to be present.
5253
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
5354
},
55+
nil, // no required scopes
56+
nil, // no accepted scopes
5457
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {
5558
client, err := deps.GetClient(ctx)
5659
if err != nil {
@@ -110,7 +113,7 @@ type OrganizationTeams struct {
110113
}
111114

112115
func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
113-
return NewTool(
116+
return NewToolWithScopes(
114117
ToolsetMetadataContext,
115118
mcp.Tool{
116119
Name: "get_teams",
@@ -129,6 +132,8 @@ func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
129132
},
130133
},
131134
},
135+
scopes.ToStringSlice(scopes.ReadOrg),
136+
scopes.ToStringSlice(scopes.ReadOrg, scopes.WriteOrg, scopes.AdminOrg),
132137
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
133138
user, err := OptionalParam[string](args, "user")
134139
if err != nil {
@@ -207,7 +212,7 @@ func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
207212
}
208213

209214
func GetTeamMembers(t translations.TranslationHelperFunc) inventory.ServerTool {
210-
return NewTool(
215+
return NewToolWithScopes(
211216
ToolsetMetadataContext,
212217
mcp.Tool{
213218
Name: "get_team_members",
@@ -231,6 +236,8 @@ func GetTeamMembers(t translations.TranslationHelperFunc) inventory.ServerTool {
231236
Required: []string{"org", "team_slug"},
232237
},
233238
},
239+
scopes.ToStringSlice(scopes.ReadOrg),
240+
scopes.ToStringSlice(scopes.ReadOrg, scopes.WriteOrg, scopes.AdminOrg),
234241
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
235242
org, err := RequiredParam[string](args, "org")
236243
if err != nil {

pkg/github/dependencies.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,24 @@ func NewTool[In, Out any](toolset inventory.ToolsetMetadata, tool mcp.Tool, hand
155155
})
156156
}
157157

158+
// NewToolWithScopes creates a ServerTool with OAuth scope requirements.
159+
// This is like NewTool but also accepts required and accepted scopes.
160+
func NewToolWithScopes[In, Out any](
161+
toolset inventory.ToolsetMetadata,
162+
tool mcp.Tool,
163+
requiredScopes []string,
164+
acceptedScopes []string,
165+
handler func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error),
166+
) inventory.ServerTool {
167+
st := inventory.NewServerToolWithContextHandler(tool, toolset, func(ctx context.Context, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error) {
168+
deps := MustDepsFromContext(ctx)
169+
return handler(ctx, deps, req, args)
170+
})
171+
st.RequiredScopes = requiredScopes
172+
st.AcceptedScopes = acceptedScopes
173+
return st
174+
}
175+
158176
// NewToolFromHandler creates a ServerTool that retrieves ToolDependencies from context at call time.
159177
// Use this when you have a handler that conforms to mcp.ToolHandler directly.
160178
//
@@ -166,3 +184,21 @@ func NewToolFromHandler(toolset inventory.ToolsetMetadata, tool mcp.Tool, handle
166184
return handler(ctx, deps, req)
167185
})
168186
}
187+
188+
// NewToolFromHandlerWithScopes creates a ServerTool with OAuth scope requirements.
189+
// This is like NewToolFromHandler but also accepts required and accepted scopes.
190+
func NewToolFromHandlerWithScopes(
191+
toolset inventory.ToolsetMetadata,
192+
tool mcp.Tool,
193+
requiredScopes []string,
194+
acceptedScopes []string,
195+
handler func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest) (*mcp.CallToolResult, error),
196+
) inventory.ServerTool {
197+
st := inventory.NewServerToolWithRawContextHandler(tool, toolset, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
198+
deps := MustDepsFromContext(ctx)
199+
return handler(ctx, deps, req)
200+
})
201+
st.RequiredScopes = requiredScopes
202+
st.AcceptedScopes = acceptedScopes
203+
return st
204+
}

pkg/inventory/server_tool.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ type ServerTool struct {
7070
// The context carries request-scoped information for the consumer to use.
7171
// Returns (enabled, error). On error, the tool should be treated as disabled.
7272
Enabled func(ctx context.Context) (bool, error)
73+
74+
// RequiredScopes specifies the minimum OAuth scopes required for this tool.
75+
// These are the scopes that must be present for the tool to function.
76+
RequiredScopes []string
77+
78+
// AcceptedScopes specifies all OAuth scopes that can be used with this tool.
79+
// This includes the required scopes plus any higher-level scopes that provide
80+
// the necessary permissions due to scope hierarchy.
81+
AcceptedScopes []string
7382
}
7483

7584
// IsReadOnly returns true if this tool is marked as read-only via annotations.

pkg/scopes/scopes.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package scopes
2+
3+
// Scope represents a GitHub OAuth scope.
4+
// These constants define all OAuth scopes used by the GitHub MCP server tools.
5+
// See https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps
6+
type Scope string
7+
8+
const (
9+
// Repo grants full control of private repositories
10+
Repo Scope = "repo"
11+
12+
// PublicRepo grants access to public repositories
13+
PublicRepo Scope = "public_repo"
14+
15+
// ReadOrg grants read-only access to organization membership, teams, and projects
16+
ReadOrg Scope = "read:org"
17+
18+
// WriteOrg grants write access to organization membership and teams
19+
WriteOrg Scope = "write:org"
20+
21+
// AdminOrg grants full control of organizations and teams
22+
AdminOrg Scope = "admin:org"
23+
24+
// Gist grants write access to gists
25+
Gist Scope = "gist"
26+
27+
// Notifications grants access to notifications
28+
Notifications Scope = "notifications"
29+
30+
// ReadProject grants read-only access to projects
31+
ReadProject Scope = "read:project"
32+
33+
// Project grants full control of projects
34+
Project Scope = "project"
35+
36+
// SecurityEvents grants read and write access to security events
37+
SecurityEvents Scope = "security_events"
38+
)
39+
40+
// ScopeSet represents a set of OAuth scopes.
41+
type ScopeSet map[Scope]bool
42+
43+
// NewScopeSet creates a new ScopeSet from the given scopes.
44+
func NewScopeSet(scopes ...Scope) ScopeSet {
45+
set := make(ScopeSet)
46+
for _, scope := range scopes {
47+
set[scope] = true
48+
}
49+
return set
50+
}
51+
52+
// ToSlice converts a ScopeSet to a slice of Scope values.
53+
func (s ScopeSet) ToSlice() []Scope {
54+
scopes := make([]Scope, 0, len(s))
55+
for scope := range s {
56+
scopes = append(scopes, scope)
57+
}
58+
return scopes
59+
}
60+
61+
// ToStringSlice converts a ScopeSet to a slice of string values.
62+
func (s ScopeSet) ToStringSlice() []string {
63+
scopes := make([]string, 0, len(s))
64+
for scope := range s {
65+
scopes = append(scopes, string(scope))
66+
}
67+
return scopes
68+
}
69+
70+
// ToStringSlice converts a slice of Scopes to a slice of strings.
71+
func ToStringSlice(scopes ...Scope) []string {
72+
result := make([]string, len(scopes))
73+
for i, scope := range scopes {
74+
result[i] = string(scope)
75+
}
76+
return result
77+
}

0 commit comments

Comments
 (0)