Add ability to view branches for a repo #141 (#205)

* Add ability to view branches for a repo #141

* fix: update ListBranches test to use InputSchema and correct translation helper

* fix: update ListBranches test to use InputSchema and correct translation helper

* fix: update ListBranches test to handle errors in tool result

* fix: replace deprecated github.String with github.Ptr

* docs: add list_branches tool documentation to README
This commit is contained in:
Arya Soni
2025-04-11 18:01:29 +05:30
committed by GitHub
parent 3c18a342c9
commit 01aefd3d7a
4 changed files with 181 additions and 0 deletions
+7
View File
@@ -311,6 +311,13 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- `branch`: Branch name (string, optional)
- `sha`: File SHA if updating (string, optional)
- **list_branches** - List branches in a GitHub repository
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `page`: Page number (number, optional)
- `perPage`: Results per page (number, optional)
- **push_files** - Push multiple files in a single commit
- `owner`: Repository owner (string, required)
+63
View File
@@ -150,6 +150,69 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t
}
}
// ListBranches creates a tool to list branches in a GitHub repository.
func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_branches",
mcp.WithDescription(t("TOOL_LIST_BRANCHES_DESCRIPTION", "List branches in a GitHub repository")),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
WithPagination(),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := requiredParam[string](request, "owner")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
repo, err := requiredParam[string](request, "repo")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
pagination, err := OptionalPaginationParams(request)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
opts := &github.BranchListOptions{
ListOptions: github.ListOptions{
Page: pagination.page,
PerPage: pagination.perPage,
},
}
client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts)
if err != nil {
return nil, fmt.Errorf("failed to list branches: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return mcp.NewToolResultError(fmt.Sprintf("failed to list branches: %s", string(body))), nil
}
r, err := json.Marshal(branches)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}
return mcp.NewToolResultText(string(r)), nil
}
}
// CreateOrUpdateFile creates a tool to create or update a file in a GitHub repository.
func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("create_or_update_file",
+110
View File
@@ -1423,3 +1423,113 @@ func Test_PushFiles(t *testing.T) {
})
}
}
func Test_ListBranches(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
tool, _ := ListBranches(stubGetClientFn(mockClient), translations.NullTranslationHelper)
assert.Equal(t, "list_branches", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "owner")
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "page")
assert.Contains(t, tool.InputSchema.Properties, "perPage")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
// Setup mock branches for success case
mockBranches := []*github.Branch{
{
Name: github.Ptr("main"),
Commit: &github.RepositoryCommit{SHA: github.Ptr("abc123")},
},
{
Name: github.Ptr("develop"),
Commit: &github.RepositoryCommit{SHA: github.Ptr("def456")},
},
}
// Test cases
tests := []struct {
name string
args map[string]interface{}
mockResponses []mock.MockBackendOption
wantErr bool
errContains string
}{
{
name: "success",
args: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"page": float64(2),
},
mockResponses: []mock.MockBackendOption{
mock.WithRequestMatch(
mock.GetReposBranchesByOwnerByRepo,
mockBranches,
),
},
wantErr: false,
},
{
name: "missing owner",
args: map[string]interface{}{
"repo": "repo",
},
mockResponses: []mock.MockBackendOption{},
wantErr: false,
errContains: "missing required parameter: owner",
},
{
name: "missing repo",
args: map[string]interface{}{
"owner": "owner",
},
mockResponses: []mock.MockBackendOption{},
wantErr: false,
errContains: "missing required parameter: repo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create mock client
mockClient := github.NewClient(mock.NewMockedHTTPClient(tt.mockResponses...))
_, handler := ListBranches(stubGetClientFn(mockClient), translations.NullTranslationHelper)
// Create request
request := createMCPRequest(tt.args)
// Call handler
result, err := handler(context.Background(), request)
if tt.wantErr {
require.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
return
}
require.NoError(t, err)
require.NotNil(t, result)
if tt.errContains != "" {
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, tt.errContains)
return
}
textContent := getTextResult(t, result)
require.NotEmpty(t, textContent.Text)
// Verify response
var branches []*github.Branch
err = json.Unmarshal([]byte(textContent.Text), &branches)
require.NoError(t, err)
assert.Len(t, branches, 2)
assert.Equal(t, "main", *branches[0].Name)
assert.Equal(t, "develop", *branches[1].Name)
})
}
}
+1
View File
@@ -70,6 +70,7 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati
s.AddTool(GetFileContents(getClient, t))
s.AddTool(GetCommit(getClient, t))
s.AddTool(ListCommits(getClient, t))
s.AddTool(ListBranches(getClient, t))
if !readOnly {
s.AddTool(CreateOrUpdateFile(getClient, t))
s.AddTool(CreateRepository(getClient, t))