Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ vendor
bin/

# macOS
.DS_Store
.DS_Store

# binary
github-mcp-server
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,13 @@ The following sets of tools are available (all are on by default):
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `repo`: Repository name (string, required)

- **list_starred_repositories** - List starred repositories
- `direction`: The direction to sort the results by. (string, optional)
- `page`: Page number for pagination (min 1) (number, optional)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `sort`: How to sort the results. Can be either 'created' (when the repository was starred) or 'updated' (when the repository was last pushed to). (string, optional)
- `username`: Username to list starred repositories for. Defaults to the authenticated user. (string, optional)

- **list_tags** - List tags
- `owner`: Repository owner (string, required)
- `page`: Page number for pagination (min 1) (number, optional)
Expand All @@ -901,6 +908,14 @@ The following sets of tools are available (all are on by default):
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `query`: Repository search query. Examples: 'machine learning in:name stars:>1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering. (string, required)

- **star_repository** - Star repository
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)

- **unstar_repository** - Unstar repository
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)

</details>

<details>
Expand Down
Binary file removed github-mcp-server
Binary file not shown.
44 changes: 44 additions & 0 deletions pkg/github/__toolsnaps__/list_starred_repositories.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"annotations": {
"title": "List starred repositories",
"readOnlyHint": true
},
"description": "List starred repositories",
"inputSchema": {
"properties": {
"direction": {
"description": "The direction to sort the results by.",
"enum": [
"asc",
"desc"
],
"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"
},
"sort": {
"description": "How to sort the results. Can be either 'created' (when the repository was starred) or 'updated' (when the repository was last pushed to).",
"enum": [
"created",
"updated"
],
"type": "string"
},
"username": {
"description": "Username to list starred repositories for. Defaults to the authenticated user.",
"type": "string"
}
},
"type": "object"
},
"name": "list_starred_repositories"
}
25 changes: 25 additions & 0 deletions pkg/github/__toolsnaps__/star_repository.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"annotations": {
"title": "Star repository",
"readOnlyHint": false
},
"description": "Star a GitHub repository",
"inputSchema": {
"properties": {
"owner": {
"description": "Repository owner",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"owner",
"repo"
],
"type": "object"
},
"name": "star_repository"
}
25 changes: 25 additions & 0 deletions pkg/github/__toolsnaps__/unstar_repository.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"annotations": {
"title": "Unstar repository",
"readOnlyHint": false
},
"description": "Unstar a GitHub repository",
"inputSchema": {
"properties": {
"owner": {
"description": "Repository owner",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"owner",
"repo"
],
"type": "object"
},
"name": "unstar_repository"
}
228 changes: 228 additions & 0 deletions pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -1683,3 +1683,231 @@ func resolveGitReference(ctx context.Context, githubClient *github.Client, owner
sha = reference.GetObject().GetSHA()
return &raw.ContentOpts{Ref: ref, SHA: sha}, nil
}

// ListStarredRepositories creates a tool to list starred repositories for the authenticated user or a specified user.
func ListStarredRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_starred_repositories",
mcp.WithDescription(t("TOOL_LIST_STARRED_REPOSITORIES_DESCRIPTION", "List starred repositories")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_LIST_STARRED_REPOSITORIES_USER_TITLE", "List starred repositories"),
ReadOnlyHint: ToBoolPtr(true),
}),
mcp.WithString("username",
mcp.Description("Username to list starred repositories for. Defaults to the authenticated user."),
),
mcp.WithString("sort",
mcp.Description("How to sort the results. Can be either 'created' (when the repository was starred) or 'updated' (when the repository was last pushed to)."),
mcp.Enum("created", "updated"),
),
mcp.WithString("direction",
mcp.Description("The direction to sort the results by."),
mcp.Enum("asc", "desc"),
),
WithPagination(),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
username, err := OptionalParam[string](request, "username")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
sort, err := OptionalParam[string](request, "sort")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
direction, err := OptionalParam[string](request, "direction")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
pagination, err := OptionalPaginationParams(request)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

opts := &github.ActivityListStarredOptions{
ListOptions: github.ListOptions{
Page: pagination.Page,
PerPage: pagination.PerPage,
},
}
if sort != "" {
opts.Sort = sort
}
if direction != "" {
opts.Direction = direction
}

client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

var repos []*github.StarredRepository
var resp *github.Response
if username == "" {
// List starred repositories for the authenticated user
repos, resp, err = client.Activity.ListStarred(ctx, "", opts)
} else {
// List starred repositories for a specific user
repos, resp, err = client.Activity.ListStarred(ctx, username, opts)
}

if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
fmt.Sprintf("failed to list starred repositories for user '%s'", username),
resp,
err,
), nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != 200 {
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 starred repositories: %s", string(body))), nil
}

// Convert to minimal format
minimalRepos := make([]MinimalRepository, 0, len(repos))
for _, starredRepo := range repos {
repo := starredRepo.Repository
minimalRepo := MinimalRepository{
ID: repo.GetID(),
Name: repo.GetName(),
FullName: repo.GetFullName(),
Description: repo.GetDescription(),
HTMLURL: repo.GetHTMLURL(),
Language: repo.GetLanguage(),
Stars: repo.GetStargazersCount(),
Forks: repo.GetForksCount(),
OpenIssues: repo.GetOpenIssuesCount(),
Private: repo.GetPrivate(),
Fork: repo.GetFork(),
Archived: repo.GetArchived(),
DefaultBranch: repo.GetDefaultBranch(),
}

if repo.UpdatedAt != nil {
minimalRepo.UpdatedAt = repo.UpdatedAt.Format("2006-01-02T15:04:05Z")
}

minimalRepos = append(minimalRepos, minimalRepo)
}
Comment on lines +1771 to +1796
Copy link

Copilot AI Sep 12, 2025

Choose a reason for hiding this comment

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

The MinimalRepository struct is missing a StarredAt field that should be populated from starredRepo.StarredAt. The PR description mentions adding a starred_at field to track when repositories were starred, but this field is not being set in the conversion logic.

Copilot uses AI. Check for mistakes.


r, err := json.Marshal(minimalRepos)
if err != nil {
return nil, fmt.Errorf("failed to marshal starred repositories: %w", err)
}

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

// StarRepository creates a tool to star a repository.
func StarRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("star_repository",
mcp.WithDescription(t("TOOL_STAR_REPOSITORY_DESCRIPTION", "Star a GitHub repository")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_STAR_REPOSITORY_USER_TITLE", "Star repository"),
ReadOnlyHint: ToBoolPtr(false),
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
),
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
}

client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

resp, err := client.Activity.Star(ctx, owner, repo)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
fmt.Sprintf("failed to star repository %s/%s", owner, repo),
resp,
err,
), nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != 204 {
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 star repository: %s", string(body))), nil
}

return mcp.NewToolResultText(fmt.Sprintf("Successfully starred repository %s/%s", owner, repo)), nil
}
}

// UnstarRepository creates a tool to unstar a repository.
func UnstarRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("unstar_repository",
mcp.WithDescription(t("TOOL_UNSTAR_REPOSITORY_DESCRIPTION", "Unstar a GitHub repository")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_UNSTAR_REPOSITORY_USER_TITLE", "Unstar repository"),
ReadOnlyHint: ToBoolPtr(false),
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
),
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
}

client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

resp, err := client.Activity.Unstar(ctx, owner, repo)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
fmt.Sprintf("failed to unstar repository %s/%s", owner, repo),
resp,
err,
), nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != 204 {
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 unstar repository: %s", string(body))), nil
}

return mcp.NewToolResultText(fmt.Sprintf("Successfully unstarred repository %s/%s", owner, repo)), nil
}
}
Loading
Loading