* 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:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user