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

Commit0e7debc

Browse files
Implement list repositories
1 parent01aefd3 commit0e7debc

File tree

3 files changed

+306
-0
lines changed

3 files changed

+306
-0
lines changed

‎pkg/github/organizations.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
9+
"github.com/github/github-mcp-server/pkg/translations"
10+
"github.com/google/go-github/v69/github"
11+
"github.com/mark3labs/mcp-go/mcp"
12+
"github.com/mark3labs/mcp-go/server"
13+
)
14+
15+
// ListCommits creates a tool to get commits of a branch in a repository.
16+
funcListRepositories(client*github.Client,t translations.TranslationHelperFunc) (tool mcp.Tool,handler server.ToolHandlerFunc) {
17+
returnmcp.NewTool("list_repositories",
18+
mcp.WithDescription(t("TOOL_LIST_REPOSITORIES_DESCRIPTION","Get list of repositories in a GitHub organization")),
19+
mcp.WithString("org",
20+
mcp.Required(),
21+
mcp.Description("Organization name"),
22+
),
23+
mcp.WithString("type",
24+
mcp.Description("Type of repositories to list. Possible values are: all, public, private, forks, sources, member. Default is 'all'."),
25+
),
26+
mcp.WithString("sort",
27+
mcp.Description("How to sort the repository list. Can be one of created, updated, pushed, full_name. Default is 'created'"),
28+
),
29+
mcp.WithString("direction",
30+
mcp.Description("Direction in which to sort repositories. Can be one of asc or desc. Default when using full_name: asc; otherwise desc."),
31+
),
32+
WithPagination(),
33+
),
34+
func(ctx context.Context,request mcp.CallToolRequest) (*mcp.CallToolResult,error) {
35+
org,err:=requiredParam[string](request,"org")
36+
iferr!=nil {
37+
returnmcp.NewToolResultError(err.Error()),nil
38+
}
39+
pagination,err:=OptionalPaginationParams(request)
40+
iferr!=nil {
41+
returnmcp.NewToolResultError(err.Error()),nil
42+
}
43+
44+
opts:=&github.RepositoryListByOrgOptions{
45+
ListOptions: github.ListOptions{
46+
Page:pagination.page,
47+
PerPage:pagination.perPage,
48+
},
49+
}
50+
51+
repo_type,err:=OptionalParam[string](request,"type")
52+
iferr!=nil {
53+
returnmcp.NewToolResultError(err.Error()),nil
54+
}
55+
ifrepo_type!="" {
56+
opts.Type=repo_type
57+
}
58+
sort,err:=OptionalParam[string](request,"sort")
59+
iferr!=nil {
60+
returnmcp.NewToolResultError(err.Error()),nil
61+
}
62+
ifsort!="" {
63+
opts.Sort=sort
64+
}
65+
direction,err:=OptionalParam[string](request,"direction")
66+
iferr!=nil {
67+
returnmcp.NewToolResultError(err.Error()),nil
68+
}
69+
ifdirection!="" {
70+
opts.Direction=direction
71+
}
72+
73+
repos,resp,err:=client.Repositories.ListByOrg(ctx,org,opts)
74+
iferr!=nil {
75+
returnnil,fmt.Errorf("failed to list repositories: %w",err)
76+
}
77+
deferfunc() {_=resp.Body.Close() }()
78+
79+
ifresp.StatusCode!=200 {
80+
body,err:=io.ReadAll(resp.Body)
81+
iferr!=nil {
82+
returnnil,fmt.Errorf("failed to read response body: %w",err)
83+
}
84+
returnmcp.NewToolResultError(fmt.Sprintf("failed to list repositories: %s",string(body))),nil
85+
}
86+
87+
r,err:=json.Marshal(repos)
88+
iferr!=nil {
89+
returnnil,fmt.Errorf("failed to marshal response: %w",err)
90+
}
91+
92+
returnmcp.NewToolResultText(string(r)),nil
93+
}
94+
}

‎pkg/github/organizations_test.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/github/github-mcp-server/pkg/translations"
10+
"github.com/google/go-github/v69/github"
11+
"github.com/migueleliasweb/go-github-mock/src/mock"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
funcTest_ListRepositories(t*testing.T) {
17+
// Verify tool definition once
18+
mockClient:=github.NewClient(nil)
19+
tool,_:=ListRepositories(mockClient,translations.NullTranslationHelper)
20+
21+
assert.Equal(t,"list_repositories",tool.Name)
22+
assert.NotEmpty(t,tool.Description)
23+
assert.Contains(t,tool.InputSchema.Properties,"org")
24+
assert.Contains(t,tool.InputSchema.Properties,"type")
25+
assert.Contains(t,tool.InputSchema.Properties,"sort")
26+
assert.Contains(t,tool.InputSchema.Properties,"direction")
27+
assert.Contains(t,tool.InputSchema.Properties,"perPage")
28+
assert.Contains(t,tool.InputSchema.Properties,"page")
29+
assert.ElementsMatch(t,tool.InputSchema.Required, []string{"org"})
30+
31+
// Setup mock repos for success case
32+
mockRepos:= []*github.Repository{
33+
{
34+
ID:github.Ptr(int64(1001)),
35+
Name:github.Ptr("repo1"),
36+
FullName:github.Ptr("testorg/repo1"),
37+
Description:github.Ptr("Test repo 1"),
38+
HTMLURL:github.Ptr("https://github.com/testorg/repo1"),
39+
Private:github.Ptr(false),
40+
Fork:github.Ptr(false),
41+
},
42+
{
43+
ID:github.Ptr(int64(1002)),
44+
Name:github.Ptr("repo2"),
45+
FullName:github.Ptr("testorg/repo2"),
46+
Description:github.Ptr("Test repo 2"),
47+
HTMLURL:github.Ptr("https://github.com/testorg/repo2"),
48+
Private:github.Ptr(true),
49+
Fork:github.Ptr(false),
50+
},
51+
}
52+
53+
tests:= []struct {
54+
namestring
55+
mockedClient*http.Client
56+
requestArgsmap[string]interface{}
57+
expectErrorbool
58+
expectedRepos []*github.Repository
59+
expectedErrMsgstring
60+
}{
61+
{
62+
name:"successful repositories listing",
63+
mockedClient:mock.NewMockedHTTPClient(
64+
mock.WithRequestMatchHandler(
65+
mock.GetOrgsReposByOrg,
66+
expectQueryParams(t,map[string]string{
67+
"type":"all",
68+
"sort":"created",
69+
"direction":"desc",
70+
"per_page":"30",
71+
"page":"1",
72+
}).andThen(
73+
mockResponse(t,http.StatusOK,mockRepos),
74+
),
75+
),
76+
),
77+
requestArgs:map[string]interface{}{
78+
"org":"testorg",
79+
"type":"all",
80+
"sort":"created",
81+
"direction":"desc",
82+
"perPage":float64(30),
83+
"page":float64(1),
84+
},
85+
expectError:false,
86+
expectedRepos:mockRepos,
87+
},
88+
{
89+
name:"successful repos listing with defaults",
90+
mockedClient:mock.NewMockedHTTPClient(
91+
mock.WithRequestMatchHandler(
92+
mock.GetOrgsReposByOrg,
93+
expectQueryParams(t,map[string]string{
94+
"per_page":"30",
95+
"page":"1",
96+
}).andThen(
97+
mockResponse(t,http.StatusOK,mockRepos),
98+
),
99+
),
100+
),
101+
requestArgs:map[string]interface{}{
102+
"org":"testorg",
103+
// Using defaults for other parameters
104+
},
105+
expectError:false,
106+
expectedRepos:mockRepos,
107+
},
108+
{
109+
name:"custom pagination and filtering",
110+
mockedClient:mock.NewMockedHTTPClient(
111+
mock.WithRequestMatchHandler(
112+
mock.GetOrgsReposByOrg,
113+
expectQueryParams(t,map[string]string{
114+
"type":"public",
115+
"sort":"updated",
116+
"direction":"asc",
117+
"per_page":"10",
118+
"page":"2",
119+
}).andThen(
120+
mockResponse(t,http.StatusOK,mockRepos),
121+
),
122+
),
123+
),
124+
requestArgs:map[string]interface{}{
125+
"org":"testorg",
126+
"type":"public",
127+
"sort":"updated",
128+
"direction":"asc",
129+
"perPage":float64(10),
130+
"page":float64(2),
131+
},
132+
expectError:false,
133+
expectedRepos:mockRepos,
134+
},
135+
{
136+
name:"API error response",
137+
mockedClient:mock.NewMockedHTTPClient(
138+
mock.WithRequestMatchHandler(
139+
mock.GetOrgsReposByOrg,
140+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
141+
w.WriteHeader(http.StatusNotFound)
142+
_,_=w.Write([]byte(`{"message": "Not Found"}`))
143+
}),
144+
),
145+
),
146+
requestArgs:map[string]interface{}{
147+
"org":"nonexistentorg",
148+
},
149+
expectError:true,
150+
expectedErrMsg:"failed to list repositories",
151+
},
152+
{
153+
name:"rate limit exceeded",
154+
mockedClient:mock.NewMockedHTTPClient(
155+
mock.WithRequestMatchHandler(
156+
mock.GetOrgsReposByOrg,
157+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
158+
w.WriteHeader(http.StatusForbidden)
159+
_,_=w.Write([]byte(`{"message": "API rate limit exceeded"}`))
160+
}),
161+
),
162+
),
163+
requestArgs:map[string]interface{}{
164+
"org":"testorg",
165+
},
166+
expectError:true,
167+
expectedErrMsg:"failed to list repositories",
168+
},
169+
}
170+
171+
for_,tc:=rangetests {
172+
t.Run(tc.name,func(t*testing.T) {
173+
// Setup client with mock
174+
client:=github.NewClient(tc.mockedClient)
175+
_,handler:=ListRepositories(client,translations.NullTranslationHelper)
176+
177+
// Create call request
178+
request:=createMCPRequest(tc.requestArgs)
179+
180+
// Call handler
181+
result,err:=handler(context.Background(),request)
182+
183+
// Verify results
184+
iftc.expectError {
185+
require.Error(t,err)
186+
assert.Contains(t,err.Error(),tc.expectedErrMsg)
187+
return
188+
}
189+
190+
require.NoError(t,err)
191+
192+
// Parse the result and get the text content if no error
193+
textContent:=getTextResult(t,result)
194+
195+
// Unmarshal and verify the result
196+
varreturnedRepos []*github.Repository
197+
err=json.Unmarshal([]byte(textContent.Text),&returnedRepos)
198+
require.NoError(t,err)
199+
assert.Len(t,returnedRepos,len(tc.expectedRepos))
200+
fori,repo:=rangereturnedRepos {
201+
assert.Equal(t,*tc.expectedRepos[i].ID,*repo.ID)
202+
assert.Equal(t,*tc.expectedRepos[i].Name,*repo.Name)
203+
assert.Equal(t,*tc.expectedRepos[i].FullName,*repo.FullName)
204+
assert.Equal(t,*tc.expectedRepos[i].Private,*repo.Private)
205+
assert.Equal(t,*tc.expectedRepos[i].HTMLURL,*repo.HTMLURL)
206+
}
207+
})
208+
}
209+
}

‎pkg/github/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati
7979
s.AddTool(PushFiles(getClient,t))
8080
}
8181

82+
// Add GitHub tools - Organizations
83+
s.AddTool(ListRepositories(client,t))
84+
8285
// Add GitHub tools - Search
8386
s.AddTool(SearchCode(getClient,t))
8487
s.AddTool(SearchUsers(getClient,t))

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp