Skip to content

Commit 883e0f4

Browse files
authored
Merge branch 'main' into prcomments
2 parents c2b2dc1 + 8343fa5 commit 883e0f4

File tree

5 files changed

+184
-8
lines changed

5 files changed

+184
-8
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[fork]: https://github.com/github/github-mcp-server/fork
44
[pr]: https://github.com/github/github-mcp-server/compare
5-
[style]: https://github.com/github/github-mcp-server/blob/main/.golangci.yaml
5+
[style]: https://github.com/github/github-mcp-server/blob/main/.golangci.yml
66

77
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
88

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,13 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
326326
- `branch`: Branch name (string, optional)
327327
- `sha`: File SHA if updating (string, optional)
328328

329+
- **list_branches** - List branches in a GitHub repository
330+
331+
- `owner`: Repository owner (string, required)
332+
- `repo`: Repository name (string, required)
333+
- `page`: Page number (number, optional)
334+
- `perPage`: Results per page (number, optional)
335+
329336
- **push_files** - Push multiple files in a single commit
330337

331338
- `owner`: Repository owner (string, required)

pkg/github/repositories.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,69 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t
150150
}
151151
}
152152

153+
// ListBranches creates a tool to list branches in a GitHub repository.
154+
func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
155+
return mcp.NewTool("list_branches",
156+
mcp.WithDescription(t("TOOL_LIST_BRANCHES_DESCRIPTION", "List branches in a GitHub repository")),
157+
mcp.WithString("owner",
158+
mcp.Required(),
159+
mcp.Description("Repository owner"),
160+
),
161+
mcp.WithString("repo",
162+
mcp.Required(),
163+
mcp.Description("Repository name"),
164+
),
165+
WithPagination(),
166+
),
167+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
168+
owner, err := requiredParam[string](request, "owner")
169+
if err != nil {
170+
return mcp.NewToolResultError(err.Error()), nil
171+
}
172+
repo, err := requiredParam[string](request, "repo")
173+
if err != nil {
174+
return mcp.NewToolResultError(err.Error()), nil
175+
}
176+
pagination, err := OptionalPaginationParams(request)
177+
if err != nil {
178+
return mcp.NewToolResultError(err.Error()), nil
179+
}
180+
181+
opts := &github.BranchListOptions{
182+
ListOptions: github.ListOptions{
183+
Page: pagination.page,
184+
PerPage: pagination.perPage,
185+
},
186+
}
187+
188+
client, err := getClient(ctx)
189+
if err != nil {
190+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
191+
}
192+
193+
branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts)
194+
if err != nil {
195+
return nil, fmt.Errorf("failed to list branches: %w", err)
196+
}
197+
defer func() { _ = resp.Body.Close() }()
198+
199+
if resp.StatusCode != http.StatusOK {
200+
body, err := io.ReadAll(resp.Body)
201+
if err != nil {
202+
return nil, fmt.Errorf("failed to read response body: %w", err)
203+
}
204+
return mcp.NewToolResultError(fmt.Sprintf("failed to list branches: %s", string(body))), nil
205+
}
206+
207+
r, err := json.Marshal(branches)
208+
if err != nil {
209+
return nil, fmt.Errorf("failed to marshal response: %w", err)
210+
}
211+
212+
return mcp.NewToolResultText(string(r)), nil
213+
}
214+
}
215+
153216
// CreateOrUpdateFile creates a tool to create or update a file in a GitHub repository.
154217
func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
155218
return mcp.NewTool("create_or_update_file",

pkg/github/repositories_test.go

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -517,11 +517,6 @@ func Test_GetCommit(t *testing.T) {
517517
},
518518
},
519519
}
520-
// This one currently isn't defined in the mock package we're using.
521-
var mockEndpointPattern = mock.EndpointPattern{
522-
Pattern: "/repos/{owner}/{repo}/commits/{sha}",
523-
Method: "GET",
524-
}
525520

526521
tests := []struct {
527522
name string
@@ -535,7 +530,7 @@ func Test_GetCommit(t *testing.T) {
535530
name: "successful commit fetch",
536531
mockedClient: mock.NewMockedHTTPClient(
537532
mock.WithRequestMatchHandler(
538-
mockEndpointPattern,
533+
mock.GetReposCommitsByOwnerByRepoByRef,
539534
mockResponse(t, http.StatusOK, mockCommit),
540535
),
541536
),
@@ -551,7 +546,7 @@ func Test_GetCommit(t *testing.T) {
551546
name: "commit fetch fails",
552547
mockedClient: mock.NewMockedHTTPClient(
553548
mock.WithRequestMatchHandler(
554-
mockEndpointPattern,
549+
mock.GetReposCommitsByOwnerByRepoByRef,
555550
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
556551
w.WriteHeader(http.StatusNotFound)
557552
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
@@ -1423,3 +1418,113 @@ func Test_PushFiles(t *testing.T) {
14231418
})
14241419
}
14251420
}
1421+
1422+
func Test_ListBranches(t *testing.T) {
1423+
// Verify tool definition once
1424+
mockClient := github.NewClient(nil)
1425+
tool, _ := ListBranches(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1426+
1427+
assert.Equal(t, "list_branches", tool.Name)
1428+
assert.NotEmpty(t, tool.Description)
1429+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1430+
assert.Contains(t, tool.InputSchema.Properties, "repo")
1431+
assert.Contains(t, tool.InputSchema.Properties, "page")
1432+
assert.Contains(t, tool.InputSchema.Properties, "perPage")
1433+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
1434+
1435+
// Setup mock branches for success case
1436+
mockBranches := []*github.Branch{
1437+
{
1438+
Name: github.Ptr("main"),
1439+
Commit: &github.RepositoryCommit{SHA: github.Ptr("abc123")},
1440+
},
1441+
{
1442+
Name: github.Ptr("develop"),
1443+
Commit: &github.RepositoryCommit{SHA: github.Ptr("def456")},
1444+
},
1445+
}
1446+
1447+
// Test cases
1448+
tests := []struct {
1449+
name string
1450+
args map[string]interface{}
1451+
mockResponses []mock.MockBackendOption
1452+
wantErr bool
1453+
errContains string
1454+
}{
1455+
{
1456+
name: "success",
1457+
args: map[string]interface{}{
1458+
"owner": "owner",
1459+
"repo": "repo",
1460+
"page": float64(2),
1461+
},
1462+
mockResponses: []mock.MockBackendOption{
1463+
mock.WithRequestMatch(
1464+
mock.GetReposBranchesByOwnerByRepo,
1465+
mockBranches,
1466+
),
1467+
},
1468+
wantErr: false,
1469+
},
1470+
{
1471+
name: "missing owner",
1472+
args: map[string]interface{}{
1473+
"repo": "repo",
1474+
},
1475+
mockResponses: []mock.MockBackendOption{},
1476+
wantErr: false,
1477+
errContains: "missing required parameter: owner",
1478+
},
1479+
{
1480+
name: "missing repo",
1481+
args: map[string]interface{}{
1482+
"owner": "owner",
1483+
},
1484+
mockResponses: []mock.MockBackendOption{},
1485+
wantErr: false,
1486+
errContains: "missing required parameter: repo",
1487+
},
1488+
}
1489+
1490+
for _, tt := range tests {
1491+
t.Run(tt.name, func(t *testing.T) {
1492+
// Create mock client
1493+
mockClient := github.NewClient(mock.NewMockedHTTPClient(tt.mockResponses...))
1494+
_, handler := ListBranches(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1495+
1496+
// Create request
1497+
request := createMCPRequest(tt.args)
1498+
1499+
// Call handler
1500+
result, err := handler(context.Background(), request)
1501+
if tt.wantErr {
1502+
require.Error(t, err)
1503+
if tt.errContains != "" {
1504+
assert.Contains(t, err.Error(), tt.errContains)
1505+
}
1506+
return
1507+
}
1508+
1509+
require.NoError(t, err)
1510+
require.NotNil(t, result)
1511+
1512+
if tt.errContains != "" {
1513+
textContent := getTextResult(t, result)
1514+
assert.Contains(t, textContent.Text, tt.errContains)
1515+
return
1516+
}
1517+
1518+
textContent := getTextResult(t, result)
1519+
require.NotEmpty(t, textContent.Text)
1520+
1521+
// Verify response
1522+
var branches []*github.Branch
1523+
err = json.Unmarshal([]byte(textContent.Text), &branches)
1524+
require.NoError(t, err)
1525+
assert.Len(t, branches, 2)
1526+
assert.Equal(t, "main", *branches[0].Name)
1527+
assert.Equal(t, "develop", *branches[1].Name)
1528+
})
1529+
}
1530+
}

pkg/github/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati
7171
s.AddTool(GetFileContents(getClient, t))
7272
s.AddTool(GetCommit(getClient, t))
7373
s.AddTool(ListCommits(getClient, t))
74+
s.AddTool(ListBranches(getClient, t))
7475
if !readOnly {
7576
s.AddTool(CreateOrUpdateFile(getClient, t))
7677
s.AddTool(CreateRepository(getClient, t))

0 commit comments

Comments
 (0)