Skip to content

Commit c428f72

Browse files
Don't filter read-only repo tools (work on public repos without scope)
1 parent c809766 commit c428f72

File tree

2 files changed

+56
-0
lines changed

2 files changed

+56
-0
lines changed

pkg/github/scope_filter.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ import (
77
"github.com/github/github-mcp-server/pkg/scopes"
88
)
99

10+
// repoScopesSet contains scopes that grant access to repository content.
11+
// Tools requiring only these scopes work on public repos without any token scope,
12+
// so we don't filter them out even if the token lacks repo/public_repo.
13+
var repoScopesSet = map[string]bool{
14+
string(scopes.Repo): true,
15+
string(scopes.PublicRepo): true,
16+
}
17+
18+
// onlyRequiresRepoScopes returns true if all of the tool's accepted scopes
19+
// are repo-related scopes (repo, public_repo). Such tools work on public
20+
// repositories without needing any scope.
21+
func onlyRequiresRepoScopes(acceptedScopes []string) bool {
22+
if len(acceptedScopes) == 0 {
23+
return false
24+
}
25+
for _, scope := range acceptedScopes {
26+
if !repoScopesSet[scope] {
27+
return false
28+
}
29+
}
30+
return true
31+
}
32+
1033
// CreateToolScopeFilter creates an inventory.ToolFilter that filters tools
1134
// based on the token's OAuth scopes.
1235
//
@@ -19,6 +42,7 @@ import (
1942
//
2043
// The filter returns true (include tool) if:
2144
// - The tool has no scope requirements (AcceptedScopes is empty)
45+
// - The tool is read-only and only requires repo/public_repo scopes (works on public repos)
2246
// - The token has at least one of the tool's accepted scopes
2347
//
2448
// Example usage:
@@ -31,6 +55,10 @@ import (
3155
// inventory := github.NewInventory(t).WithFilter(filter).Build()
3256
func CreateToolScopeFilter(tokenScopes []string) inventory.ToolFilter {
3357
return func(_ context.Context, tool *inventory.ServerTool) (bool, error) {
58+
// Read-only tools requiring only repo/public_repo work on public repos without any scope
59+
if tool.Tool.Annotations != nil && tool.Tool.Annotations.ReadOnlyHint && onlyRequiresRepoScopes(tool.AcceptedScopes) {
60+
return true, nil
61+
}
3462
return scopes.HasRequiredScopes(tokenScopes, tool.AcceptedScopes), nil
3563
}
3664
}

pkg/github/scope_filter_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,27 @@ func TestCreateToolScopeFilter(t *testing.T) {
2727
AcceptedScopes: []string{"repo"},
2828
}
2929

30+
toolRepoScopeReadOnly := &inventory.ServerTool{
31+
Tool: mcp.Tool{
32+
Name: "repo_tool_readonly",
33+
Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true},
34+
},
35+
AcceptedScopes: []string{"repo"},
36+
}
37+
3038
toolPublicRepoScope := &inventory.ServerTool{
3139
Tool: mcp.Tool{Name: "public_repo_tool"},
3240
AcceptedScopes: []string{"public_repo", "repo"}, // repo is parent, also accepted
3341
}
3442

43+
toolPublicRepoScopeReadOnly := &inventory.ServerTool{
44+
Tool: mcp.Tool{
45+
Name: "public_repo_tool_readonly",
46+
Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true},
47+
},
48+
AcceptedScopes: []string{"public_repo", "repo"},
49+
}
50+
3551
toolGistScope := &inventory.ServerTool{
3652
Tool: mcp.Tool{Name: "gist_tool"},
3753
AcceptedScopes: []string{"gist"},
@@ -96,6 +112,18 @@ func TestCreateToolScopeFilter(t *testing.T) {
96112
tool: toolRepoScope,
97113
expected: false,
98114
},
115+
{
116+
name: "empty token scopes CAN see read-only repo tools (public repos)",
117+
tokenScopes: []string{},
118+
tool: toolRepoScopeReadOnly,
119+
expected: true,
120+
},
121+
{
122+
name: "empty token scopes CAN see read-only public_repo tools",
123+
tokenScopes: []string{},
124+
tool: toolPublicRepoScopeReadOnly,
125+
expected: true,
126+
},
99127
{
100128
name: "token with multiple scopes where one matches",
101129
tokenScopes: []string{"gist", "repo"},

0 commit comments

Comments
 (0)