Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit4f1dee2

Browse files
Add content filtering functionality and tests
Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
1 parente21c81b commit4f1dee2

File tree

4 files changed

+742
-0
lines changed

4 files changed

+742
-0
lines changed

‎cmd/github-mcp-server/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ var (
5353
ExportTranslations:viper.GetBool("export-translations"),
5454
EnableCommandLogging:viper.GetBool("enable-command-logging"),
5555
LogFilePath:viper.GetString("log-file"),
56+
TrustedRepo:viper.GetString("trusted-repo"),
5657
}
5758

5859
returnghmcp.RunStdioServer(stdioServerConfig)
@@ -73,6 +74,7 @@ func init() {
7374
rootCmd.PersistentFlags().Bool("enable-command-logging",false,"When enabled, the server will log all command requests and responses to the log file")
7475
rootCmd.PersistentFlags().Bool("export-translations",false,"Save translations to a JSON file")
7576
rootCmd.PersistentFlags().String("gh-host","","Specify the GitHub hostname (for GitHub Enterprise etc.)")
77+
rootCmd.PersistentFlags().String("trusted-repo","","Limit content to users with push access to the specified repo (format: owner/repo)")
7678

7779
// Bind flag to viper
7880
_=viper.BindPFlag("toolsets",rootCmd.PersistentFlags().Lookup("toolsets"))
@@ -82,6 +84,7 @@ func init() {
8284
_=viper.BindPFlag("enable-command-logging",rootCmd.PersistentFlags().Lookup("enable-command-logging"))
8385
_=viper.BindPFlag("export-translations",rootCmd.PersistentFlags().Lookup("export-translations"))
8486
_=viper.BindPFlag("host",rootCmd.PersistentFlags().Lookup("gh-host"))
87+
_=viper.BindPFlag("trusted-repo",rootCmd.PersistentFlags().Lookup("trusted-repo"))
8588

8689
// Add subcommands
8790
rootCmd.AddCommand(stdioCmd)

‎internal/ghmcp/server.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,17 @@ type MCPServerConfig struct {
4343
// ReadOnly indicates if we should only offer read-only tools
4444
ReadOnlybool
4545

46+
// TrustedRepo is a repository in the format "owner/repo" used to limit content
47+
// to users with push access to the specified repo
48+
TrustedRepostring
49+
4650
// Translator provides translated text for the server tooling
4751
Translator translations.TranslationHelperFunc
4852
}
4953

5054
funcNewMCPServer(cfgMCPServerConfig) (*server.MCPServer,error) {
55+
ctx:=context.Background()
56+
5157
apiHost,err:=parseAPIHost(cfg.Host)
5258
iferr!=nil {
5359
returnnil,fmt.Errorf("failed to parse API host: %w",err)
@@ -112,6 +118,14 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
112118
returngqlClient,nil// closing over client
113119
}
114120

121+
// Initialize the content filter if a trusted repo is specified
122+
ifcfg.TrustedRepo!="" {
123+
ctx,err:=InitContentFilter(ctx,cfg.TrustedRepo,getGQLClient)
124+
iferr!=nil {
125+
returnnil,fmt.Errorf("failed to initialize content filter: %w",err)
126+
}
127+
}
128+
115129
// Create default toolsets
116130
toolsets,err:=github.InitToolsets(
117131
enabledToolsets,
@@ -169,6 +183,10 @@ type StdioServerConfig struct {
169183

170184
// Path to the log file if not stderr
171185
LogFilePathstring
186+
187+
// TrustedRepo is a repository in the format "owner/repo" used to limit content
188+
// to users with push access to the specified repo
189+
TrustedRepostring
172190
}
173191

174192
// RunStdioServer is not concurrent safe.
@@ -186,6 +204,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
186204
EnabledToolsets:cfg.EnabledToolsets,
187205
DynamicToolsets:cfg.DynamicToolsets,
188206
ReadOnly:cfg.ReadOnly,
207+
TrustedRepo:cfg.TrustedRepo,
189208
Translator:t,
190209
})
191210
iferr!=nil {

‎pkg/github/content_filter.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"sync"
8+
9+
"github.com/shurcooL/githubv4"
10+
)
11+
12+
// contextKey is a private type used for context keys
13+
typecontextKeyint
14+
15+
const (
16+
// ContentFilterKey is the key used to access content filter settings from context
17+
contentFilterKeycontextKey=iota
18+
)
19+
20+
// ContentFilterSettings holds the configuration for content filtering
21+
typeContentFilterSettingsstruct {
22+
// Enabled indicates if content filtering is enabled
23+
Enabledbool
24+
// TrustedRepo is the repository in format "owner/repo" that is used to check permissions
25+
TrustedRepostring
26+
// OwnerRepo is the parsed owner and repo from TrustedRepo
27+
OwnerRepoOwnerRepo
28+
// IsPrivate indicates if the trusted repo is private
29+
IsPrivatebool
30+
// TrustedUsers is a map of users who have been verified to have push access
31+
TrustedUsersmap[string]bool
32+
// mu protects the TrustedUsers map
33+
mu sync.RWMutex
34+
}
35+
36+
// OwnerRepo holds the parsed owner and repo from a string in the format "owner/repo"
37+
typeOwnerRepostruct {
38+
Ownerstring
39+
Repostring
40+
}
41+
42+
// ParseOwnerRepo parses a string in the format "owner/repo" into an OwnerRepo struct
43+
funcParseOwnerRepo(sstring) (OwnerRepo,error) {
44+
parts:=strings.Split(s,"/")
45+
iflen(parts)!=2||parts[0]==""||parts[1]=="" {
46+
returnOwnerRepo{},fmt.Errorf("invalid format for owner/repo: %s",s)
47+
}
48+
returnOwnerRepo{Owner:parts[0],Repo:parts[1]},nil
49+
}
50+
51+
// GetContentFilterFromContext retrieves the content filter settings from the context
52+
funcGetContentFilterFromContext(ctx context.Context) (*ContentFilterSettings,bool) {
53+
ifctx==nil {
54+
returnnil,false
55+
}
56+
settings,ok:=ctx.Value(contentFilterKey).(*ContentFilterSettings)
57+
returnsettings,ok
58+
}
59+
60+
// InitContentFilter initializes the content filter in the context
61+
funcInitContentFilter(ctx context.Context,trustedRepostring,getGQLClientGetGQLClientFn) (context.Context,error) {
62+
iftrustedRepo=="" {
63+
// Content filtering is not enabled
64+
returnctx,nil
65+
}
66+
67+
ownerRepo,err:=ParseOwnerRepo(trustedRepo)
68+
iferr!=nil {
69+
returnctx,err
70+
}
71+
72+
settings:=&ContentFilterSettings{
73+
Enabled:true,
74+
TrustedRepo:trustedRepo,
75+
OwnerRepo:ownerRepo,
76+
TrustedUsers:map[string]bool{},
77+
}
78+
79+
// Check if the repository is private, if so, disable content filtering
80+
isPrivate,err:=IsRepoPrivate(ctx,settings.OwnerRepo,getGQLClient)
81+
iferr!=nil {
82+
returnctx,fmt.Errorf("failed to check repository visibility: %w",err)
83+
}
84+
settings.IsPrivate=isPrivate
85+
86+
returncontext.WithValue(ctx,contentFilterKey,settings),nil
87+
}
88+
89+
// IsRepoPrivate checks if a repository is private using GraphQL
90+
funcIsRepoPrivate(ctx context.Context,ownerRepoOwnerRepo,getGQLClientGetGQLClientFn) (bool,error) {
91+
client,err:=getGQLClient(ctx)
92+
iferr!=nil {
93+
returnfalse,fmt.Errorf("failed to get GraphQL client: %w",err)
94+
}
95+
96+
varquerystruct {
97+
Repositorystruct {
98+
IsPrivate githubv4.Boolean
99+
}`graphql:"repository(owner: $owner, name: $name)"`
100+
}
101+
102+
variables:=map[string]interface{}{
103+
"owner":githubv4.String(ownerRepo.Owner),
104+
"name":githubv4.String(ownerRepo.Repo),
105+
}
106+
107+
err=client.Query(ctx,&query,variables)
108+
iferr!=nil {
109+
returnfalse,fmt.Errorf("failed to query repository visibility: %w",err)
110+
}
111+
112+
returnbool(query.Repository.IsPrivate),nil
113+
}
114+
115+
// HasPushAccess checks if a user has push access to the trusted repository
116+
funcHasPushAccess(ctx context.Context,usernamestring,getGQLClientGetGQLClientFn) (bool,error) {
117+
settings,ok:=GetContentFilterFromContext(ctx)
118+
if!ok||!settings.Enabled||settings.IsPrivate {
119+
// If filtering is not enabled or repo is private, all users are trusted
120+
returntrue,nil
121+
}
122+
123+
// Check cache first
124+
settings.mu.RLock()
125+
trusted,found:=settings.TrustedUsers[username]
126+
settings.mu.RUnlock()
127+
iffound {
128+
returntrusted,nil
129+
}
130+
131+
// Query GitHub API for permission
132+
client,err:=getGQLClient(ctx)
133+
iferr!=nil {
134+
returnfalse,fmt.Errorf("failed to get GraphQL client: %w",err)
135+
}
136+
137+
varquerystruct {
138+
Repositorystruct {
139+
Collaboratorsstruct {
140+
Edges []struct {
141+
Permission githubv4.String
142+
Nodestruct {
143+
Login githubv4.String
144+
}
145+
}
146+
}`graphql:"collaborators(query: $username, first: 1)"`
147+
}`graphql:"repository(owner: $owner, name: $name)"`
148+
}
149+
150+
variables:=map[string]interface{}{
151+
"owner":githubv4.String(settings.OwnerRepo.Owner),
152+
"name":githubv4.String(settings.OwnerRepo.Repo),
153+
"username":githubv4.String(username),
154+
}
155+
156+
err=client.Query(ctx,&query,variables)
157+
iferr!=nil {
158+
returnfalse,fmt.Errorf("failed to query user permissions: %w",err)
159+
}
160+
161+
// Check if the user has push access
162+
hasPush:=false
163+
for_,edge:=rangequery.Repository.Collaborators.Edges {
164+
login:=string(edge.Node.Login)
165+
ifstrings.EqualFold(login,username) {
166+
permission:=string(edge.Permission)
167+
// WRITE, ADMIN, and MAINTAIN permissions have push access
168+
hasPush=permission=="WRITE"||permission=="ADMIN"||permission=="MAINTAIN"
169+
break
170+
}
171+
}
172+
173+
// Cache the result
174+
settings.mu.Lock()
175+
settings.TrustedUsers[username]=hasPush
176+
settings.mu.Unlock()
177+
178+
returnhasPush,nil
179+
}
180+
181+
// ShouldIncludeContent checks if content from a user should be included
182+
funcShouldIncludeContent(ctx context.Context,usernamestring,getGQLClientGetGQLClientFn)bool {
183+
settings,ok:=GetContentFilterFromContext(ctx)
184+
if!ok||!settings.Enabled||settings.IsPrivate {
185+
// If filtering is not enabled or repo is private, include all content
186+
returntrue
187+
}
188+
189+
// Check if user has push access
190+
hasPush,err:=HasPushAccess(ctx,username,getGQLClient)
191+
iferr!=nil {
192+
// If there's an error checking permissions, default to not including the content for safety
193+
returnfalse
194+
}
195+
returnhasPush
196+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp