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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy