Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: add list repository contributors and update readme#893

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
beccccaboo wants to merge6 commits intogithub:main
base:main
Choose a base branch
Loading
frombeccccaboo:feature/add-list-repository-contributors
Open
Show file tree
Hide file tree
Changes fromall commits
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
6 changes: 6 additions & 0 deletionsREADME.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -873,6 +873,12 @@ The following sets of tools are available (all are on by default):
- `repo`: Repository name (string, required)
- `sha`: Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA. (string, optional)

- **list_repository_contributors** - List repository contributors
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `page`: Page number for pagination (min 1) (number, optional)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)

- **list_releases** - List releases
- `owner`: Repository owner (string, required)
- `page`: Page number for pagination (min 1) (number, optional)
Expand Down
Binary file modifiedgithub-mcp-server
View file
Open in desktop
Binary file not shown.
36 changes: 36 additions & 0 deletionspkg/github/__toolsnaps__/list_repository_contributors.snap
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
{
"annotations": {
"title": "List repository contributors",
"readOnlyHint": true
},
"description": "Get list of contributors for a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).",
"inputSchema": {
"properties": {
"owner": {
"description": "Repository owner",
"type": "string"
},
"page": {
"description": "Page number for pagination (min 1)",
"minimum": 1,
"type": "number"
},
"perPage": {
"description": "Results per page for pagination (min 1, max 100)",
"maximum": 100,
"minimum": 1,
"type": "number"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"owner",
"repo"
],
"type": "object"
},
"name": "list_repository_contributors"
}
70 changes: 70 additions & 0 deletionspkg/github/repositories.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,6 +200,76 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t
}
}

// ListRepositoryContributors creates a tool to get contributors of a repository.
func ListRepositoryContributors(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_repository_contributors",
mcp.WithDescription(t("TOOL_LIST_REPOSITORY_CONTRIBUTORS_DESCRIPTION", "Get list of contributors for a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_LIST_REPOSITORY_CONTRIBUTORS_USER_TITLE", "List repository contributors"),
ReadOnlyHint: ToBoolPtr(true),
}),
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.ListContributorsOptions{
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)
}
contributors, resp, err := client.Repositories.ListContributors(ctx, owner, repo, opts)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
fmt.Sprintf("failed to list contributors for repository: %s/%s", owner, repo),
resp,
err,
), nil
}
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 contributors: %s", string(body))), nil
}

Comment on lines +256 to +263
Copy link

CopilotAISep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

The status code check and error handling is redundant since the GitHub client API call already handles HTTP errors. This pattern differs from other similar functions in the codebase likeListCommits andListBranches where the status check is performed after confirming no API error occurred. Consider removing lines 256-262 to maintain consistency.

Suggested change
ifresp.StatusCode!= http.StatusOK {
body,err:=io.ReadAll(resp.Body)
iferr!=nil {
returnnil,fmt.Errorf("failed to read response body: %w",err)
}
returnmcp.NewToolResultError(fmt.Sprintf("failed to list contributors: %s",string(body))),nil
}

Copilot uses AI. Check for mistakes.
r, err := json.Marshal(contributors)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}
}

// 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",
Expand Down
183 changes: 183 additions & 0 deletionspkg/github/repositories_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2866,3 +2866,186 @@ func Test_resolveGitReference(t *testing.T) {
})
}
}

func Test_ListRepositoryContributors(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
tool, _ := ListRepositoryContributors(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))

assert.Equal(t, "list_repository_contributors", 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 contributors for success case
mockContributors := []*github.Contributor{
{
Login: github.Ptr("user1"),
ID: github.Int64(1),
NodeID: github.Ptr("MDQ6VXNlcjE="),
AvatarURL: github.Ptr("https://github.com/images/error/user1_happy.gif"),
GravatarID: github.Ptr(""),
URL: github.Ptr("https://api.github.com/users/user1"),
HTMLURL: github.Ptr("https://github.com/user1"),
FollowersURL: github.Ptr("https://api.github.com/users/user1/followers"),
FollowingURL: github.Ptr("https://api.github.com/users/user1/following{/other_user}"),
GistsURL: github.Ptr("https://api.github.com/users/user1/gists{/gist_id}"),
StarredURL: github.Ptr("https://api.github.com/users/user1/starred{/owner}{/repo}"),
SubscriptionsURL: github.Ptr("https://api.github.com/users/user1/subscriptions"),
OrganizationsURL: github.Ptr("https://api.github.com/users/user1/orgs"),
ReposURL: github.Ptr("https://api.github.com/users/user1/repos"),
EventsURL: github.Ptr("https://api.github.com/users/user1/events{/privacy}"),
ReceivedEventsURL: github.Ptr("https://api.github.com/users/user1/received_events"),
Type: github.Ptr("User"),
SiteAdmin: github.Bool(false),
Contributions: github.Int(42),
},
{
Login: github.Ptr("user2"),
ID: github.Int64(2),
NodeID: github.Ptr("MDQ6VXNlcjI="),
AvatarURL: github.Ptr("https://github.com/images/error/user2_happy.gif"),
GravatarID: github.Ptr(""),
URL: github.Ptr("https://api.github.com/users/user2"),
HTMLURL: github.Ptr("https://github.com/user2"),
FollowersURL: github.Ptr("https://api.github.com/users/user2/followers"),
FollowingURL: github.Ptr("https://api.github.com/users/user2/following{/other_user}"),
GistsURL: github.Ptr("https://api.github.com/users/user2/gists{/gist_id}"),
StarredURL: github.Ptr("https://api.github.com/users/user2/starred{/owner}{/repo}"),
SubscriptionsURL: github.Ptr("https://api.github.com/users/user2/subscriptions"),
OrganizationsURL: github.Ptr("https://api.github.com/users/user2/orgs"),
ReposURL: github.Ptr("https://api.github.com/users/user2/repos"),
EventsURL: github.Ptr("https://api.github.com/users/user2/events{/privacy}"),
ReceivedEventsURL: github.Ptr("https://api.github.com/users/user2/received_events"),
Type: github.Ptr("User"),
SiteAdmin: github.Bool(false),
Contributions: github.Int(15),
},
}

tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectedContributors []*github.Contributor
expectedErrMsg string
}{
{
name: "successful contributors fetch with default params",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.GetReposContributorsByOwnerByRepo,
mockContributors,
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
},
expectError: false,
expectedContributors: mockContributors,
},
{
name: "successful contributors fetch with pagination",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposContributorsByOwnerByRepo,
expectQueryParams(t, map[string]string{
"page": "2",
"per_page": "50",
}).andThen(
mockResponse(t, http.StatusOK, mockContributors),
),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"page": float64(2),
"perPage": float64(50),
},
expectError: false,
expectedContributors: mockContributors,
},
{
name: "missing required parameter owner",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"repo": "repo",
},
expectError: true,
expectedErrMsg: "missing required parameter: owner",
},
{
name: "missing required parameter repo",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]interface{}{
"owner": "owner",
},
expectError: true,
expectedErrMsg: "missing required parameter: repo",
},
{
name: "GitHub API error",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposContributorsByOwnerByRepo,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
},
expectError: true,
expectedErrMsg: "failed to list contributors for repository: owner/repo",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ListRepositoryContributors(stubGetClientFn(client), translations.NullTranslationHelper)

// Create call request
request := createMCPRequest(tc.requestArgs)

// Call handler
result, err := handler(context.Background(), request)

// Verify results
if tc.expectError {
require.NoError(t, err)
require.True(t, result.IsError)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
return
}

require.NoError(t, err)
require.False(t, result.IsError)

// Parse the result and get the text content if no error
textContent := getTextResult(t, result)

// Unmarshal and verify the result
var returnedContributors []*github.Contributor
err = json.Unmarshal([]byte(textContent.Text), &returnedContributors)
require.NoError(t, err)
assert.Len(t, returnedContributors, len(tc.expectedContributors))
for i, contributor := range returnedContributors {
assert.Equal(t, tc.expectedContributors[i].GetLogin(), contributor.GetLogin())
assert.Equal(t, tc.expectedContributors[i].GetContributions(), contributor.GetContributions())
}
})
}
}
1 change: 1 addition & 0 deletionspkg/github/tools.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(SearchRepositories(getClient, t)),
toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)),
toolsets.NewServerTool(ListCommits(getClient, t)),
toolsets.NewServerTool(ListRepositoryContributors(getClient, t)),
toolsets.NewServerTool(SearchCode(getClient, t)),
toolsets.NewServerTool(GetCommit(getClient, t)),
toolsets.NewServerTool(ListBranches(getClient, t)),
Expand Down
17 changes: 17 additions & 0 deletionsscript/list-repository-contributors
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
#!/bin/bash

# Test script for list_repository_contributors function
# Usage: ./script/list-repository-contributors <owner> <repo>

if [ $# -ne 2 ]; then
echo "Usage: $0 <owner> <repo>"
echo "Example: $0 octocat Hello-World"
exit 1
fi

OWNER=$1
REPO=$2

echo "Testing list_repository_contributors for $OWNER/$REPO"

echo "{\"jsonrpc\":\"2.0\",\"id\":1,\"params\":{\"name\":\"list_repository_contributors\",\"arguments\":{\"owner\":\"$OWNER\",\"repo\":\"$REPO\"}},\"method\":\"tools/call\"}" | go run cmd/github-mcp-server/main.go stdio | jq .

[8]ページ先頭

©2009-2025 Movatter.jp