Skip to content

Commit 4460948

Browse files
Copilottonytrg
andcommitted
Implement starred repository functionality with comprehensive tests
- Add ListStarredRepositories tool for listing starred repos - Add StarRepository tool for starring repositories - Add UnstarRepository tool for unstarring repositories - Update MinimalRepository struct with StarredAt field - Add comprehensive test coverage for all new functionality - Register new tools in the repos toolset Co-authored-by: tonytrg <40869903+tonytrg@users.noreply.github.com>
1 parent 4742fa6 commit 4460948

File tree

8 files changed

+656
-0
lines changed

8 files changed

+656
-0
lines changed

github-mcp-server

51.3 KB
Binary file not shown.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"annotations": {
3+
"title": "List starred repositories",
4+
"readOnlyHint": true
5+
},
6+
"description": "List repositories starred by the authenticated user or a specified user",
7+
"inputSchema": {
8+
"properties": {
9+
"direction": {
10+
"description": "The direction to sort the results by.",
11+
"enum": [
12+
"asc",
13+
"desc"
14+
],
15+
"type": "string"
16+
},
17+
"page": {
18+
"description": "Page number for pagination (min 1)",
19+
"minimum": 1,
20+
"type": "number"
21+
},
22+
"perPage": {
23+
"description": "Results per page for pagination (min 1, max 100)",
24+
"maximum": 100,
25+
"minimum": 1,
26+
"type": "number"
27+
},
28+
"sort": {
29+
"description": "How to sort the results. Can be either 'created' (when the repository was starred) or 'updated' (when the repository was last pushed to).",
30+
"enum": [
31+
"created",
32+
"updated"
33+
],
34+
"type": "string"
35+
},
36+
"username": {
37+
"description": "Username to list starred repositories for. If not provided, lists starred repositories for the authenticated user.",
38+
"type": "string"
39+
}
40+
},
41+
"type": "object"
42+
},
43+
"name": "list_starred_repositories"
44+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"annotations": {
3+
"title": "Star repository",
4+
"readOnlyHint": false
5+
},
6+
"description": "Star a GitHub repository",
7+
"inputSchema": {
8+
"properties": {
9+
"owner": {
10+
"description": "Repository owner",
11+
"type": "string"
12+
},
13+
"repo": {
14+
"description": "Repository name",
15+
"type": "string"
16+
}
17+
},
18+
"required": [
19+
"owner",
20+
"repo"
21+
],
22+
"type": "object"
23+
},
24+
"name": "star_repository"
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"annotations": {
3+
"title": "Unstar repository",
4+
"readOnlyHint": false
5+
},
6+
"description": "Unstar a GitHub repository",
7+
"inputSchema": {
8+
"properties": {
9+
"owner": {
10+
"description": "Repository owner",
11+
"type": "string"
12+
},
13+
"repo": {
14+
"description": "Repository name",
15+
"type": "string"
16+
}
17+
},
18+
"required": [
19+
"owner",
20+
"repo"
21+
],
22+
"type": "object"
23+
},
24+
"name": "unstar_repository"
25+
}

pkg/github/minimal_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type MinimalRepository struct {
3131
OpenIssues int `json:"open_issues_count"`
3232
UpdatedAt string `json:"updated_at,omitempty"`
3333
CreatedAt string `json:"created_at,omitempty"`
34+
StarredAt string `json:"starred_at,omitempty"`
3435
Topics []string `json:"topics,omitempty"`
3536
Private bool `json:"private"`
3637
Fork bool `json:"fork"`

pkg/github/repositories.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,3 +1683,234 @@ func resolveGitReference(ctx context.Context, githubClient *github.Client, owner
16831683
sha = reference.GetObject().GetSHA()
16841684
return &raw.ContentOpts{Ref: ref, SHA: sha}, nil
16851685
}
1686+
1687+
// ListStarredRepositories creates a tool to list starred repositories for the authenticated user or a specified user.
1688+
func ListStarredRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1689+
return mcp.NewTool("list_starred_repositories",
1690+
mcp.WithDescription(t("TOOL_LIST_STARRED_REPOSITORIES_DESCRIPTION", "List repositories starred by the authenticated user or a specified user")),
1691+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
1692+
Title: t("TOOL_LIST_STARRED_REPOSITORIES_USER_TITLE", "List starred repositories"),
1693+
ReadOnlyHint: ToBoolPtr(true),
1694+
}),
1695+
mcp.WithString("username",
1696+
mcp.Description("Username to list starred repositories for. If not provided, lists starred repositories for the authenticated user."),
1697+
),
1698+
mcp.WithString("sort",
1699+
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)."),
1700+
mcp.Enum("created", "updated"),
1701+
),
1702+
mcp.WithString("direction",
1703+
mcp.Description("The direction to sort the results by."),
1704+
mcp.Enum("asc", "desc"),
1705+
),
1706+
WithPagination(),
1707+
),
1708+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
1709+
username, err := OptionalParam[string](request, "username")
1710+
if err != nil {
1711+
return mcp.NewToolResultError(err.Error()), nil
1712+
}
1713+
sort, err := OptionalParam[string](request, "sort")
1714+
if err != nil {
1715+
return mcp.NewToolResultError(err.Error()), nil
1716+
}
1717+
direction, err := OptionalParam[string](request, "direction")
1718+
if err != nil {
1719+
return mcp.NewToolResultError(err.Error()), nil
1720+
}
1721+
pagination, err := OptionalPaginationParams(request)
1722+
if err != nil {
1723+
return mcp.NewToolResultError(err.Error()), nil
1724+
}
1725+
1726+
opts := &github.ActivityListStarredOptions{
1727+
ListOptions: github.ListOptions{
1728+
Page: pagination.Page,
1729+
PerPage: pagination.PerPage,
1730+
},
1731+
}
1732+
if sort != "" {
1733+
opts.Sort = sort
1734+
}
1735+
if direction != "" {
1736+
opts.Direction = direction
1737+
}
1738+
1739+
client, err := getClient(ctx)
1740+
if err != nil {
1741+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
1742+
}
1743+
1744+
var repos []*github.StarredRepository
1745+
var resp *github.Response
1746+
if username == "" {
1747+
// List starred repositories for the authenticated user
1748+
repos, resp, err = client.Activity.ListStarred(ctx, "", opts)
1749+
} else {
1750+
// List starred repositories for a specific user
1751+
repos, resp, err = client.Activity.ListStarred(ctx, username, opts)
1752+
}
1753+
1754+
if err != nil {
1755+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1756+
fmt.Sprintf("failed to list starred repositories for user '%s'", username),
1757+
resp,
1758+
err,
1759+
), nil
1760+
}
1761+
defer func() { _ = resp.Body.Close() }()
1762+
1763+
if resp.StatusCode != 200 {
1764+
body, err := io.ReadAll(resp.Body)
1765+
if err != nil {
1766+
return nil, fmt.Errorf("failed to read response body: %w", err)
1767+
}
1768+
return mcp.NewToolResultError(fmt.Sprintf("failed to list starred repositories: %s", string(body))), nil
1769+
}
1770+
1771+
// Convert to minimal format
1772+
minimalRepos := make([]MinimalRepository, 0, len(repos))
1773+
for _, starredRepo := range repos {
1774+
repo := starredRepo.Repository
1775+
minimalRepo := MinimalRepository{
1776+
ID: repo.GetID(),
1777+
Name: repo.GetName(),
1778+
FullName: repo.GetFullName(),
1779+
Description: repo.GetDescription(),
1780+
HTMLURL: repo.GetHTMLURL(),
1781+
Language: repo.GetLanguage(),
1782+
Stars: repo.GetStargazersCount(),
1783+
Forks: repo.GetForksCount(),
1784+
OpenIssues: repo.GetOpenIssuesCount(),
1785+
Private: repo.GetPrivate(),
1786+
Fork: repo.GetFork(),
1787+
Archived: repo.GetArchived(),
1788+
DefaultBranch: repo.GetDefaultBranch(),
1789+
}
1790+
1791+
if repo.UpdatedAt != nil {
1792+
minimalRepo.UpdatedAt = repo.UpdatedAt.Format("2006-01-02T15:04:05Z")
1793+
}
1794+
if starredRepo.StarredAt != nil {
1795+
minimalRepo.StarredAt = starredRepo.StarredAt.Format("2006-01-02T15:04:05Z")
1796+
}
1797+
1798+
minimalRepos = append(minimalRepos, minimalRepo)
1799+
}
1800+
1801+
r, err := json.Marshal(minimalRepos)
1802+
if err != nil {
1803+
return nil, fmt.Errorf("failed to marshal starred repositories: %w", err)
1804+
}
1805+
1806+
return mcp.NewToolResultText(string(r)), nil
1807+
}
1808+
}
1809+
1810+
// StarRepository creates a tool to star a repository.
1811+
func StarRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1812+
return mcp.NewTool("star_repository",
1813+
mcp.WithDescription(t("TOOL_STAR_REPOSITORY_DESCRIPTION", "Star a GitHub repository")),
1814+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
1815+
Title: t("TOOL_STAR_REPOSITORY_USER_TITLE", "Star repository"),
1816+
ReadOnlyHint: ToBoolPtr(false),
1817+
}),
1818+
mcp.WithString("owner",
1819+
mcp.Required(),
1820+
mcp.Description("Repository owner"),
1821+
),
1822+
mcp.WithString("repo",
1823+
mcp.Required(),
1824+
mcp.Description("Repository name"),
1825+
),
1826+
),
1827+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
1828+
owner, err := RequiredParam[string](request, "owner")
1829+
if err != nil {
1830+
return mcp.NewToolResultError(err.Error()), nil
1831+
}
1832+
repo, err := RequiredParam[string](request, "repo")
1833+
if err != nil {
1834+
return mcp.NewToolResultError(err.Error()), nil
1835+
}
1836+
1837+
client, err := getClient(ctx)
1838+
if err != nil {
1839+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
1840+
}
1841+
1842+
resp, err := client.Activity.Star(ctx, owner, repo)
1843+
if err != nil {
1844+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1845+
fmt.Sprintf("failed to star repository %s/%s", owner, repo),
1846+
resp,
1847+
err,
1848+
), nil
1849+
}
1850+
defer func() { _ = resp.Body.Close() }()
1851+
1852+
if resp.StatusCode != 204 {
1853+
body, err := io.ReadAll(resp.Body)
1854+
if err != nil {
1855+
return nil, fmt.Errorf("failed to read response body: %w", err)
1856+
}
1857+
return mcp.NewToolResultError(fmt.Sprintf("failed to star repository: %s", string(body))), nil
1858+
}
1859+
1860+
return mcp.NewToolResultText(fmt.Sprintf("Successfully starred repository %s/%s", owner, repo)), nil
1861+
}
1862+
}
1863+
1864+
// UnstarRepository creates a tool to unstar a repository.
1865+
func UnstarRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1866+
return mcp.NewTool("unstar_repository",
1867+
mcp.WithDescription(t("TOOL_UNSTAR_REPOSITORY_DESCRIPTION", "Unstar a GitHub repository")),
1868+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
1869+
Title: t("TOOL_UNSTAR_REPOSITORY_USER_TITLE", "Unstar repository"),
1870+
ReadOnlyHint: ToBoolPtr(false),
1871+
}),
1872+
mcp.WithString("owner",
1873+
mcp.Required(),
1874+
mcp.Description("Repository owner"),
1875+
),
1876+
mcp.WithString("repo",
1877+
mcp.Required(),
1878+
mcp.Description("Repository name"),
1879+
),
1880+
),
1881+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
1882+
owner, err := RequiredParam[string](request, "owner")
1883+
if err != nil {
1884+
return mcp.NewToolResultError(err.Error()), nil
1885+
}
1886+
repo, err := RequiredParam[string](request, "repo")
1887+
if err != nil {
1888+
return mcp.NewToolResultError(err.Error()), nil
1889+
}
1890+
1891+
client, err := getClient(ctx)
1892+
if err != nil {
1893+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
1894+
}
1895+
1896+
resp, err := client.Activity.Unstar(ctx, owner, repo)
1897+
if err != nil {
1898+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1899+
fmt.Sprintf("failed to unstar repository %s/%s", owner, repo),
1900+
resp,
1901+
err,
1902+
), nil
1903+
}
1904+
defer func() { _ = resp.Body.Close() }()
1905+
1906+
if resp.StatusCode != 204 {
1907+
body, err := io.ReadAll(resp.Body)
1908+
if err != nil {
1909+
return nil, fmt.Errorf("failed to read response body: %w", err)
1910+
}
1911+
return mcp.NewToolResultError(fmt.Sprintf("failed to unstar repository: %s", string(body))), nil
1912+
}
1913+
1914+
return mcp.NewToolResultText(fmt.Sprintf("Successfully unstarred repository %s/%s", owner, repo)), nil
1915+
}
1916+
}

0 commit comments

Comments
 (0)