Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add ability to view branches for a repo #141
  • Loading branch information
aryasoni98 committed Apr 9, 2025
commit 69e37b570013426a76526b3719f7c6f4f3443865
58 changes: 58 additions & 0 deletions pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,64 @@ func ListCommits(client *github.Client, t translations.TranslationHelperFunc) (t
}
}

// ListBranches creates a tool to list branches in a GitHub repository.
func ListBranches(client *github.Client, 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,
},
}

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(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("create_or_update_file",
Expand Down
103 changes: 103 additions & 0 deletions pkg/github/repositories_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"

Expand Down Expand Up @@ -1293,3 +1295,104 @@ func Test_PushFiles(t *testing.T) {
})
}
}

func Test_ListBranches(t *testing.T) {
Comment thread
aryasoni98 marked this conversation as resolved.
Resolved
// Create a test server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/repos/owner/repo/branches", r.URL.Path)
assert.Equal(t, "GET", r.Method)

// Check query parameters
query := r.URL.Query()
if page := query.Get("page"); page != "" {
assert.Equal(t, "2", page)
}
if perPage := query.Get("per_page"); perPage != "" {
assert.Equal(t, "30", perPage)
}

// Return mock branches
mockBranches := []github.Branch{
{Name: github.String("main")},
{Name: github.String("develop")},
}
mockResponse(t, http.StatusOK, mockBranches)(w, r)
}))
defer ts.Close()

// Create a GitHub client using the test server URL
client := github.NewClient(nil)
client.BaseURL, _ = url.Parse(ts.URL + "/")

// Create the tool
tool, handler := ListBranches(client, translations.NullTranslationHelper)

// Test cases
tests := []struct {
name string
args map[string]interface{}
wantErr bool
errContains string
}{
{
name: "success",
args: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"page": float64(2),
},
wantErr: false,
},
{
name: "missing owner",
args: map[string]interface{}{
"repo": "repo",
},
wantErr: true,
errContains: "missing required parameter: owner",
},
{
name: "missing repo",
args: map[string]interface{}{
"owner": "owner",
},
wantErr: true,
errContains: "missing required parameter: repo",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 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)
textContent := getTextResult(t, result)

// 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)
})
}

// Verify tool definition
assert.Equal(t, "list_branches", tool.Name)
assert.Contains(t, tool.InputSchema.Required, "owner")
assert.Contains(t, tool.InputSchema.Required, "repo")
assert.NotContains(t, tool.InputSchema.Required, "page")
assert.NotContains(t, tool.InputSchema.Required, "perPage")
}
1 change: 1 addition & 0 deletions pkg/github/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati
s.AddTool(SearchRepositories(client, t))
s.AddTool(GetFileContents(client, t))
s.AddTool(ListCommits(client, t))
s.AddTool(ListBranches(client, t))
if !readOnly {
s.AddTool(CreateOrUpdateFile(client, t))
s.AddTool(CreateRepository(client, t))
Expand Down