Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHostResolv
if cfg.RepoAccessTTL != nil {
opts = append(opts, lockdown.WithTTL(*cfg.RepoAccessTTL))
}
repoAccessCache = lockdown.GetInstance(gqlClient, opts...)
repoAccessCache = lockdown.GetInstance(gqlClient, restClient, opts...)
}

return &githubClients{
Expand Down
7 changes: 6 additions & 1 deletion pkg/github/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,13 @@ func (d *RequestDeps) GetRepoAccessCache(ctx context.Context) (*lockdown.RepoAcc
return nil, err
}

restClient, err := d.GetClient(ctx)
if err != nil {
return nil, err
}

// Create repo access cache
instance := lockdown.GetInstance(gqlClient, d.RepoAccessOpts...)
instance := lockdown.GetInstance(gqlClient, restClient, d.RepoAccessOpts...)
return instance, nil
}

Expand Down
140 changes: 28 additions & 112 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (

"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/lockdown"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v82/github"
"github.com/google/jsonschema-go/jsonschema"
Expand All @@ -23,17 +22,14 @@ import (
)

var defaultGQLClient *githubv4.Client = githubv4.NewClient(newRepoAccessHTTPClient())
var repoAccessCache *lockdown.RepoAccessCache = stubRepoAccessCache(defaultGQLClient, 15*time.Minute)

type repoAccessKey struct {
owner string
repo string
username string
owner string
repo string
}

type repoAccessValue struct {
isPrivate bool
permission string
isPrivate bool
}

type repoAccessMockTransport struct {
Expand All @@ -42,8 +38,8 @@ type repoAccessMockTransport struct {

func newRepoAccessHTTPClient() *http.Client {
responses := map[repoAccessKey]repoAccessValue{
{owner: "owner2", repo: "repo2", username: "testuser2"}: {isPrivate: true},
{owner: "owner", repo: "repo", username: "testuser"}: {isPrivate: false, permission: "READ"},
{owner: "owner2", repo: "repo2"}: {isPrivate: true},
{owner: "owner", repo: "repo"}: {isPrivate: false},
}

return &http.Client{Transport: &repoAccessMockTransport{responses: responses}}
Expand All @@ -66,30 +62,19 @@ func (rt *repoAccessMockTransport) RoundTrip(req *http.Request) (*http.Response,

owner := toString(payload.Variables["owner"])
repo := toString(payload.Variables["name"])
username := toString(payload.Variables["username"])

value, ok := rt.responses[repoAccessKey{owner: owner, repo: repo, username: username}]
value, ok := rt.responses[repoAccessKey{owner: owner, repo: repo}]
if !ok {
value = repoAccessValue{isPrivate: false, permission: "WRITE"}
}

edges := []any{}
if value.permission != "" {
edges = append(edges, map[string]any{
"permission": value.permission,
"node": map[string]any{
"login": username,
},
})
value = repoAccessValue{isPrivate: false}
}

responseBody, err := json.Marshal(map[string]any{
"data": map[string]any{
"viewer": map[string]any{
"login": "test-viewer",
},
"repository": map[string]any{
"isPrivate": value.isPrivate,
"collaborators": map[string]any{
"edges": edges,
},
},
},
})
Expand Down Expand Up @@ -170,13 +155,13 @@ func Test_GetIssue(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
gqlHTTPClient *http.Client
requestArgs map[string]any
expectHandlerError bool
expectResultError bool
expectedIssue *github.Issue
expectedErrMsg string
lockdownEnabled bool
restPermission string
}{
{
name: "successful issue retrieval",
Expand Down Expand Up @@ -210,36 +195,6 @@ func Test_GetIssue(t *testing.T) {
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue2),
}),
gqlHTTPClient: githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
IsPrivate githubv4.Boolean
Collaborators struct {
Edges []struct {
Permission githubv4.String
Node struct {
Login githubv4.String
}
}
} `graphql:"collaborators(query: $username, first: 1)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}{},
map[string]any{
"owner": githubv4.String("owner2"),
"name": githubv4.String("repo2"),
"username": githubv4.String("testuser2"),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"isPrivate": true,
"collaborators": map[string]any{
"edges": []any{},
},
},
}),
),
),
requestArgs: map[string]any{
"method": "get",
"owner": "owner2",
Expand All @@ -248,49 +203,13 @@ func Test_GetIssue(t *testing.T) {
},
expectedIssue: mockIssue2,
lockdownEnabled: true,
restPermission: "none",
},
{
name: "lockdown enabled - user lacks push access",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
}),
gqlHTTPClient: githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
IsPrivate githubv4.Boolean
Collaborators struct {
Edges []struct {
Permission githubv4.String
Node struct {
Login githubv4.String
}
}
} `graphql:"collaborators(query: $username, first: 1)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"name": githubv4.String("repo"),
"username": githubv4.String("testuser"),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"isPrivate": false,
"collaborators": map[string]any{
"edges": []any{
map[string]any{
"permission": "READ",
"node": map[string]any{
"login": "testuser",
},
},
},
},
},
}),
),
),
requestArgs: map[string]any{
"method": "get",
"owner": "owner",
Expand All @@ -300,26 +219,24 @@ func Test_GetIssue(t *testing.T) {
expectResultError: true,
expectedErrMsg: "access to issue details is restricted by lockdown mode",
lockdownEnabled: true,
restPermission: "read",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)

var gqlClient *githubv4.Client
cache := repoAccessCache
if tc.gqlHTTPClient != nil {
gqlClient = githubv4.NewClient(tc.gqlHTTPClient)
cache = stubRepoAccessCache(gqlClient, 15*time.Minute)
} else {
gqlClient = githubv4.NewClient(nil)
var restClient *github.Client
if tc.restPermission != "" {
restClient = mockRESTPermissionServer(t, tc.restPermission, nil)
}
cache := stubRepoAccessCache(restClient, 15*time.Minute)

flags := stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled})
deps := BaseDeps{
Client: client,
GQLClient: gqlClient,
GQLClient: defaultGQLClient,
RepoAccessCache: cache,
Flags: flags,
}
Expand Down Expand Up @@ -1997,7 +1914,6 @@ func Test_GetIssueComments(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
gqlHTTPClient *http.Client
requestArgs map[string]any
expectError bool
expectedComments []*github.IssueComment
Expand Down Expand Up @@ -2069,7 +1985,6 @@ func Test_GetIssueComments(t *testing.T) {
},
}),
}),
gqlHTTPClient: newRepoAccessHTTPClient(),
requestArgs: map[string]any{
"method": "get_comments",
"owner": "owner",
Expand All @@ -2092,17 +2007,18 @@ func Test_GetIssueComments(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
var gqlClient *githubv4.Client
if tc.gqlHTTPClient != nil {
gqlClient = githubv4.NewClient(tc.gqlHTTPClient)
} else {
gqlClient = githubv4.NewClient(nil)
var restClient *github.Client
if tc.lockdownEnabled {
restClient = mockRESTPermissionServer(t, "read", map[string]string{
"maintainer": "write",
"testuser": "read",
})
}
cache := stubRepoAccessCache(gqlClient, 15*time.Minute)
cache := stubRepoAccessCache(restClient, 15*time.Minute)
flags := stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled})
deps := BaseDeps{
Client: client,
GQLClient: gqlClient,
GQLClient: defaultGQLClient,
RepoAccessCache: cache,
Flags: flags,
}
Expand Down Expand Up @@ -2223,7 +2139,7 @@ func Test_GetIssueLabels(t *testing.T) {
deps := BaseDeps{
Client: client,
GQLClient: gqlClient,
RepoAccessCache: stubRepoAccessCache(gqlClient, 15*time.Minute),
RepoAccessCache: stubRepoAccessCache(nil, 15*time.Minute),
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
}
handler := serverTool.Handler(deps)
Expand Down Expand Up @@ -2652,7 +2568,7 @@ func Test_GetSubIssues(t *testing.T) {
deps := BaseDeps{
Client: client,
GQLClient: gqlClient,
RepoAccessCache: stubRepoAccessCache(gqlClient, 15*time.Minute),
RepoAccessCache: stubRepoAccessCache(nil, 15*time.Minute),
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
}
handler := serverTool.Handler(deps)
Expand Down
36 changes: 19 additions & 17 deletions pkg/github/pullrequests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/lockdown"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v82/github"
"github.com/google/jsonschema-go/jsonschema"
Expand Down Expand Up @@ -101,7 +100,7 @@ func Test_GetPullRequest(t *testing.T) {
deps := BaseDeps{
Client: client,
GQLClient: gqlClient,
RepoAccessCache: stubRepoAccessCache(gqlClient, 5*time.Minute),
RepoAccessCache: stubRepoAccessCache(nil, 5*time.Minute),
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
}
handler := serverTool.Handler(deps)
Expand Down Expand Up @@ -1202,7 +1201,7 @@ func Test_GetPullRequestFiles(t *testing.T) {
serverTool := PullRequestRead(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
RepoAccessCache: stubRepoAccessCache(githubv4.NewClient(githubv4mock.NewMockedHTTPClient()), 5*time.Minute),
RepoAccessCache: stubRepoAccessCache(nil, 5*time.Minute),
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
}
handler := serverTool.Handler(deps)
Expand Down Expand Up @@ -1362,7 +1361,7 @@ func Test_GetPullRequestStatus(t *testing.T) {
serverTool := PullRequestRead(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
RepoAccessCache: stubRepoAccessCache(githubv4.NewClient(nil), 5*time.Minute),
RepoAccessCache: stubRepoAccessCache(nil, 5*time.Minute),
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
}
handler := serverTool.Handler(deps)
Expand Down Expand Up @@ -1518,7 +1517,7 @@ func Test_GetPullRequestCheckRuns(t *testing.T) {
serverTool := PullRequestRead(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
RepoAccessCache: stubRepoAccessCache(githubv4.NewClient(nil), 5*time.Minute),
RepoAccessCache: stubRepoAccessCache(nil, 5*time.Minute),
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
}
handler := serverTool.Handler(deps)
Expand Down Expand Up @@ -1937,12 +1936,15 @@ func Test_GetPullRequestComments(t *testing.T) {
}

// Setup cache for lockdown mode
var cache *lockdown.RepoAccessCache
var restClient *github.Client
if tc.lockdownEnabled {
cache = stubRepoAccessCache(githubv4.NewClient(newRepoAccessHTTPClient()), 5*time.Minute)
} else {
cache = stubRepoAccessCache(gqlClient, 5*time.Minute)
restClient = mockRESTPermissionServer(t, "read", map[string]string{
"maintainer": "write",
"external-user": "read",
"testuser": "read",
})
}
cache := stubRepoAccessCache(restClient, 5*time.Minute)

flags := stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled})
serverTool := PullRequestRead(translations.NullTranslationHelper)
Expand Down Expand Up @@ -2083,7 +2085,6 @@ func Test_GetPullRequestReviews(t *testing.T) {
},
}),
}),
gqlHTTPClient: newRepoAccessHTTPClient(),
requestArgs: map[string]any{
"method": "get_reviews",
"owner": "owner",
Expand All @@ -2107,13 +2108,14 @@ func Test_GetPullRequestReviews(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
var gqlClient *githubv4.Client
if tc.gqlHTTPClient != nil {
gqlClient = githubv4.NewClient(tc.gqlHTTPClient)
} else {
gqlClient = githubv4.NewClient(nil)
var restClient *github.Client
if tc.lockdownEnabled {
restClient = mockRESTPermissionServer(t, "read", map[string]string{
"maintainer": "write",
"testuser": "read",
})
}
cache := stubRepoAccessCache(gqlClient, 5*time.Minute)
cache := stubRepoAccessCache(restClient, 5*time.Minute)
flags := stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled})
serverTool := PullRequestRead(translations.NullTranslationHelper)
deps := BaseDeps{
Expand Down Expand Up @@ -3348,7 +3350,7 @@ index 5d6e7b2..8a4f5c3 100644
serverTool := PullRequestRead(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
RepoAccessCache: stubRepoAccessCache(githubv4.NewClient(nil), 5*time.Minute),
RepoAccessCache: stubRepoAccessCache(nil, 5*time.Minute),
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
}
handler := serverTool.Handler(deps)
Expand Down
Loading
Loading